diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+# 0.3.0.0
+
+Split out `aeson-extra`
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2015, Oleg Grenrus
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Oleg Grenrus nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,8 @@
+# aeson-compat
+
+[![Build Status](https://travis-ci.org/phadej/aeson-compat.svg?branch=master)](https://travis-ci.org/phadej/aeson-compat)
+[![Hackage](https://img.shields.io/hackage/v/aeson-compat.svg)](http://hackage.haskell.org/package/aeson-compat)
+[![Stackage LTS 3](http://stackage.org/package/monad-http/badge/lts-3)](http://stackage.org/lts-3/package/aeson-compat)
+[![Stackage Nightly](http://stackage.org/package/monad-http/badge/nightly)](http://stackage.org/nightly/package/aeson-compat)
+
+The package provides compatibility layer for [`aeson`](http://hackage.haskell.org/package/aeson)
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/aeson-compat.cabal b/aeson-compat.cabal
new file mode 100644
--- /dev/null
+++ b/aeson-compat.cabal
@@ -0,0 +1,77 @@
+-- This file has been generated from package.yaml by hpack version 0.8.0.
+--
+-- see: https://github.com/sol/hpack
+
+name:           aeson-compat
+version:        0.3.0.0
+synopsis:       Compatibility layer for aeson
+description:    Compatibility layer for @aeson@
+category:       Web
+homepage:       https://github.com/phadej/aeson-compat#readme
+bug-reports:    https://github.com/phadej/aeson-compat/issues
+author:         Oleg Grenrus <oleg.grenrus@iki.fi>
+maintainer:     Oleg Grenrus <oleg.grenrus@iki.fi>
+license:        BSD3
+license-file:   LICENSE
+tested-with:    GHC==7.6.3, GHC==7.8.4, GHC==7.10.3
+build-type:     Simple
+cabal-version:  >= 1.10
+
+extra-source-files:
+    CHANGELOG.md
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/phadej/aeson-compat
+
+library
+  hs-source-dirs:
+      src
+  ghc-options: -Wall
+  build-depends:
+      base                     >=4.6  && <4.9
+    , aeson                    >=0.7.0.6 && <0.11
+    , attoparsec               >=0.12 && <0.14
+    , bytestring               >=0.10 && <0.11
+    , containers               >=0.5  && <0.6
+    , exceptions               >=0.8  && <0.9
+    , hashable                 >=1.2  && <1.3
+    , scientific               >=0.3  && <0.4
+    , text                     >=1.2  && <1.3
+    , time                     >=1.4.2 && <1.7
+    , unordered-containers     >=0.2  && <0.3
+    , vector                   >=0.10 && <0.12
+  exposed-modules:
+      Data.Aeson.Compat
+  other-modules:
+      Data.Aeson.Compat.Time
+  default-language: Haskell2010
+
+test-suite aeson-compat-test
+  type: exitcode-stdio-1.0
+  main-is: Tests.hs
+  hs-source-dirs:
+      test
+  ghc-options: -Wall
+  build-depends:
+      base                     >=4.6  && <4.9
+    , aeson                    >=0.7.0.6 && <0.11
+    , attoparsec               >=0.12 && <0.14
+    , bytestring               >=0.10 && <0.11
+    , containers               >=0.5  && <0.6
+    , exceptions               >=0.8  && <0.9
+    , hashable                 >=1.2  && <1.3
+    , scientific               >=0.3  && <0.4
+    , text                     >=1.2  && <1.3
+    , time                     >=1.4.2 && <1.7
+    , unordered-containers     >=0.2  && <0.3
+    , vector                   >=0.10 && <0.12
+    , aeson-compat
+    , tasty                 >=0.10 && <0.12
+    , tasty-hunit           >=0.9  && <0.10
+    , tasty-quickcheck      >=0.8  && <0.9
+    , quickcheck-instances  >=0.3  && <0.4
+  other-modules:
+      Orphans
+  default-language: Haskell2010
diff --git a/src/Data/Aeson/Compat.hs b/src/Data/Aeson/Compat.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Aeson/Compat.hs
@@ -0,0 +1,194 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Data.Aeson.Compat
+-- Copyright   :  (C) 2015 Oleg Grenrus
+-- License     :  BSD3
+-- Maintainer  :  Oleg Grenrus <oleg.grenrus@iki.fi>
+--
+-- Compatibility notices
+--
+--   * 'decode' etc. work as in @aeson >=0.9@
+--   * but it is generalised to work in any 'MonadThrow' (that is extra)
+--   * '.:?' works as in @aeson <0.10@
+--   * '.:!' works as '.:?' in @aeson ==0.10@
+--   * Orphan instances 'FromJSON' 'Day' and 'FromJSON' 'LocalTime' for @aeson <0.10@
+--
+module Data.Aeson.Compat (
+  -- * Generic decoding functions
+  decode,
+  decode',
+  decodeStrict,
+  decodeStrict',
+  AesonException(..),
+  -- * Either decoding functions
+  eitherDecode, eitherDecode', eitherDecodeStrict, eitherDecodeStrict',
+  -- * Operators
+  (.:?), (.:!),
+  -- * Re-exports
+  -- | Original 'Data.Aeson..:?' operator is not re-exported
+  module Data.Aeson,
+  ) where
+
+#if !MIN_VERSION_base(4,8,0)
+import           Control.Applicative
+#endif
+
+#if MIN_VERSION_aeson(0,10,0)
+import           Data.Monoid
+#endif
+
+import           Data.Aeson hiding
+  ((.:?), decode, decode', decodeStrict, decodeStrict'
+#if !MIN_VERSION_aeson (0,9,0)
+  , eitherDecode, eitherDecode', eitherDecodeStrict, eitherDecodeStrict'
+#endif
+  )
+
+#if !MIN_VERSION_aeson (0,9,0)
+import           Data.Aeson.Parser (value, value')
+import qualified Data.Attoparsec.ByteString as A
+import qualified Data.Attoparsec.ByteString.Char8 as A (skipSpace)
+import qualified Data.Attoparsec.Lazy as L
+#endif
+
+import           Control.Monad.Catch
+import           Data.Aeson.Types hiding ((.:?))
+import           Data.ByteString as B
+import           Data.ByteString.Lazy as L
+import qualified Data.HashMap.Strict as H
+import           Data.Text as T
+import           Data.Typeable (Typeable)
+
+#if !MIN_VERSION_aeson(0,10,0)
+import           Data.Time (Day, LocalTime)
+import qualified Data.Aeson.Compat.Time as CompatTime
+#endif
+
+-- | Exception thrown by 'decode' - family of functions in this module.
+newtype AesonException = AesonException String
+  deriving (Show, Typeable)
+
+instance Exception AesonException
+
+eitherAesonExc :: (MonadThrow m) => Either String a -> m a
+eitherAesonExc (Left err) = throwM (AesonException err)
+eitherAesonExc (Right x)  = return x
+
+-- | Like original 'Data.Aeson.decode' but in arbitrary 'MonadThrow'.
+--
+-- Parse a top-level JSON value, i.e. also strings, numbers etc.
+decode :: (FromJSON a, MonadThrow m) => L.ByteString -> m a
+decode = eitherAesonExc . eitherDecode
+
+-- | Like original 'Data.Aeson.decode'' but in arbitrary 'MonadThrow'.
+decode' :: (FromJSON a, MonadThrow m) => L.ByteString -> m a
+decode' = eitherAesonExc . eitherDecode'
+
+-- | Like original 'Data.Aeson.decodeStrict' but in arbitrary 'MonadThrow'.
+decodeStrict :: (FromJSON a, MonadThrow m) => B.ByteString -> m a
+decodeStrict = eitherAesonExc . eitherDecodeStrict
+
+-- | Like original 'Data.Aeson.decodeStrict'' but in arbitrary 'MonadThrow'.
+decodeStrict' :: (FromJSON a, MonadThrow m) => B.ByteString -> m a
+decodeStrict' = eitherAesonExc . eitherDecodeStrict'
+
+-- | Retrieve the value associated with the given key of an 'Object'.
+-- The result is 'Nothing' if the key is not present, or 'empty' if
+-- the value cannot be converted to the desired type.
+--
+-- This accessor is most useful if the key and value can be absent
+-- from an object without affecting its validity.  If the key and
+-- value are mandatory, use '.:' instead.
+--
+-- This operator is consistent in @aeson >=0.7 && <0.11@
+(.:?) :: (FromJSON a) => Object -> Text -> Parser (Maybe a)
+obj .:? key = case H.lookup key obj of
+                Nothing -> pure Nothing
+                Just v  ->
+#if MIN_VERSION_aeson(0,10,0)
+                  modifyFailure addKeyName $ parseJSON v -- <?> Key key
+  where
+    addKeyName = (("failed to parse field " <> T.unpack key <> ": ") <>)
+#else
+                  parseJSON v
+#endif
+{-# INLINE (.:?) #-}
+
+-- | Like '.:?', but the resulting parser will fail,
+-- if the key is present but is 'Null'.
+(.:!) :: (FromJSON a) => Object -> Text -> Parser (Maybe a)
+obj .:! key = case H.lookup key obj of
+                Nothing -> pure Nothing
+                Just v  ->
+#if MIN_VERSION_aeson(0,10,0)
+                  modifyFailure addKeyName $ Just <$> parseJSON v -- <?> Key key
+  where
+    addKeyName = (("failed to parse field " <> T.unpack key <> ": ") <>)
+#else
+                  Just <$> parseJSON v
+#endif
+{-# INLINE (.:!) #-}
+
+#if !MIN_VERSION_aeson(0,9,0)
+-- From Parser.Internal
+
+-- | Parse a top-level JSON value followed by optional whitespace and
+-- end-of-input.  See also: 'json'.
+jsonEOF :: A.Parser Value
+jsonEOF = value <* A.skipSpace <* A.endOfInput
+
+-- | Parse a top-level JSON value followed by optional whitespace and
+-- end-of-input.  See also: 'json''.
+jsonEOF' :: A.Parser Value
+jsonEOF' = value' <* A.skipSpace <* A.endOfInput
+
+-- | Like 'decode' but returns an error message when decoding fails.
+eitherDecode :: (FromJSON a) => L.ByteString -> Either String a
+eitherDecode = eitherDecodeWith jsonEOF fromJSON
+{-# INLINE eitherDecode #-}
+
+-- | Like 'decodeStrict' but returns an error message when decoding fails.
+eitherDecodeStrict :: (FromJSON a) => B.ByteString -> Either String a
+eitherDecodeStrict = eitherDecodeStrictWith jsonEOF fromJSON
+{-# INLINE eitherDecodeStrict #-}
+
+-- | Like 'decode'' but returns an error message when decoding fails.
+eitherDecode' :: (FromJSON a) => L.ByteString -> Either String a
+eitherDecode' = eitherDecodeWith jsonEOF' fromJSON
+{-# INLINE eitherDecode' #-}
+
+-- | Like 'decodeStrict'' but returns an error message when decoding fails.
+eitherDecodeStrict' :: (FromJSON a) => B.ByteString -> Either String a
+eitherDecodeStrict' = eitherDecodeStrictWith jsonEOF' fromJSON
+{-# INLINE eitherDecodeStrict' #-}
+
+eitherDecodeWith :: L.Parser Value -> (Value -> Result a) -> L.ByteString
+                 -> Either String a
+eitherDecodeWith p to s =
+    case L.parse p s of
+      L.Done _ v -> case to v of
+                      Success a -> Right a
+                      Error msg -> Left msg
+      L.Fail _ _ msg -> Left msg
+{-# INLINE eitherDecodeWith #-}
+
+eitherDecodeStrictWith :: A.Parser Value -> (Value -> Result a) -> B.ByteString
+                       -> Either String a
+eitherDecodeStrictWith p to s =
+    case either Error to (A.parseOnly p s) of
+      Success a -> Right a
+      Error msg -> Left msg
+{-# INLINE eitherDecodeStrictWith #-}
+
+#endif
+
+#if !MIN_VERSION_aeson(0,10,0)
+instance FromJSON Day where
+  parseJSON = withText "Day" (CompatTime.run CompatTime.day)
+
+instance FromJSON LocalTime where
+  parseJSON = withText "LocalTime" (CompatTime.run CompatTime.localTime)
+#endif
diff --git a/src/Data/Aeson/Compat/Time.hs b/src/Data/Aeson/Compat/Time.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Aeson/Compat/Time.hs
@@ -0,0 +1,159 @@
+{-# LANGUAGE BangPatterns, CPP, ScopedTypeVariables #-}
+
+-- |
+-- Module:      Data.Aeson.Compat.Time (Data.Aeson.Parser.Time)
+-- Copyright:   (c) 2015 Bryan O'Sullivan
+-- License:     Apache
+-- Maintainer:  Bryan O'Sullivan <bos@serpentine.com>
+-- Stability:   experimental
+-- Portability: portable
+--
+-- Parsers for parsing dates and times.
+
+module Data.Aeson.Compat.Time
+    (
+      run
+    , day
+    , localTime
+    , timeOfDay
+    , timeZone
+    , utcTime
+    , zonedTime
+    ) where
+
+import Control.Monad (when, void)
+import Data.Attoparsec.Text as A
+import Data.Bits ((.&.))
+import Data.Char (isDigit, ord)
+import Data.Fixed (Pico)
+import Data.Int (Int64)
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import Data.Time.Calendar (Day, fromGregorianValid)
+import Data.Time.Clock (UTCTime(..))
+import qualified Data.Aeson.Types as Aeson
+import qualified Data.Text as T
+import qualified Data.Time.LocalTime as Local
+
+#if !MIN_VERSION_base(4,8,0)
+import Control.Applicative ((<$>), (<*>), (<*), (*>))
+#endif
+
+-- from Data.Aeson.Internal.Time
+import Unsafe.Coerce (unsafeCoerce)
+
+toPico :: Integer -> Pico
+toPico = unsafeCoerce
+
+-- | Run an attoparsec parser as an aeson parser.
+run :: Parser a -> Text -> Aeson.Parser a
+run p t = case A.parseOnly (p <* endOfInput) t of
+            Left err -> fail $ "could not parse date: " ++ err
+            Right r  -> return r
+
+-- | Parse a date of the form @YYYY-MM-DD@.
+day :: Parser Day
+day = do
+  y <- decimal <* char '-'
+  m <- twoDigits <* char '-'
+  d <- twoDigits
+  maybe (fail "invalid date") return (fromGregorianValid y m d)
+
+-- | Parse a two-digit integer (e.g. day of month, hour).
+twoDigits :: Parser Int
+twoDigits = do
+  a <- digit
+  b <- digit
+  let c2d c = ord c .&. 15
+  return $! c2d a * 10 + c2d b
+
+-- | Parse a time of the form @HH:MM:SS[.SSS]@.
+timeOfDay :: Parser Local.TimeOfDay
+timeOfDay = do
+  h <- twoDigits <* char ':'
+  m <- twoDigits <* char ':'
+  s <- seconds
+  if h < 24 && m < 60 && s < 61
+    then return (Local.TimeOfDay h m s)
+    else fail "invalid time"
+
+data T = T {-# UNPACK #-} !Int {-# UNPACK #-} !Int64
+
+-- | Parse a count of seconds, with the integer part being two digits
+-- long.
+seconds :: Parser Pico
+seconds = do
+  real <- twoDigits
+  mc <- peekChar
+  case mc of
+    Just '.' -> do
+      t <- anyChar *> takeWhile1 isDigit
+      return $! parsePicos real t
+    _ -> return $! fromIntegral real
+ where
+  parsePicos a0 t = toPico (fromIntegral (t' * 10^n))
+    where T n t'  = T.foldl' step (T 12 (fromIntegral a0)) t
+          step ma@(T m a) c
+              | m <= 0    = ma
+              | otherwise = T (m-1) (10 * a + fromIntegral (ord c) .&. 15)
+
+-- | Parse a time zone, and return 'Nothing' if the offset from UTC is
+-- zero. (This makes some speedups possible.)
+timeZone :: Parser (Maybe Local.TimeZone)
+timeZone = do
+  let maybeSkip c = do ch <- peekChar'; when (ch == c) (void anyChar)
+  maybeSkip ' '
+  ch <- satisfy $ \c -> c == 'Z' || c == '+' || c == '-'
+  if ch == 'Z'
+    then return Nothing
+    else do
+      h <- twoDigits
+      mm <- peekChar
+      m <- case mm of
+             Just ':'           -> anyChar *> twoDigits
+             Just d | isDigit d -> twoDigits
+             _                  -> return 0
+      let off | ch == '-' = negate off0
+              | otherwise = off0
+          off0 = h * 60 + m
+      case undefined of
+        _   | off == 0 ->
+              return Nothing
+            | off < -720 || off > 840 || m > 59 ->
+              fail "invalid time zone offset"
+            | otherwise ->
+              let !tz = Local.minutesToTimeZone off
+              in return (Just tz)
+
+-- | Parse a date and time, of the form @YYYY-MM-DD HH:MM:SS@.
+-- The space may be replaced with a @T@.  The number of seconds may be
+-- followed by a fractional component.
+localTime :: Parser Local.LocalTime
+localTime = Local.LocalTime <$> day <* daySep <*> timeOfDay
+  where daySep = satisfy (\c -> c == 'T' || c == ' ')
+
+-- | Behaves as 'zonedTime', but converts any time zone offset into a
+-- UTC time.
+utcTime :: Parser UTCTime
+utcTime = do
+  lt@(Local.LocalTime d t) <- localTime
+  mtz <- timeZone
+  case mtz of
+    Nothing -> let !tt = Local.timeOfDayToTime t
+               in return (UTCTime d tt)
+    Just tz -> return $! Local.localTimeToUTC tz lt
+
+-- | Parse a date with time zone info. Acceptable formats:
+--
+-- @YYYY-MM-DD HH:MM:SS Z@
+--
+-- The first space may instead be a @T@, and the second space is
+-- optional.  The @Z@ represents UTC.  The @Z@ may be replaced with a
+-- time zone offset of the form @+0000@ or @-08:00@, where the first
+-- two digits are hours, the @:@ is optional and the second two digits
+-- (also optional) are minutes.
+zonedTime :: Parser Local.ZonedTime
+zonedTime = Local.ZonedTime <$> localTime <*> (fromMaybe utc <$> timeZone)
+
+utc :: Local.TimeZone
+utc = Local.TimeZone 0 False ""
diff --git a/test/Orphans.hs b/test/Orphans.hs
new file mode 100644
--- /dev/null
+++ b/test/Orphans.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE CPP #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+module Orphans where
+
+#if !MIN_VERSION_base(4,8,0)
+import           Control.Applicative
+#endif
+
+import Data.Vector as V
+import Test.Tasty.QuickCheck
+
+instance Arbitrary a => Arbitrary (Vector a) where
+  arbitrary = V.fromList <$> arbitrary
+  shrink    =  fmap V.fromList . shrink . V.toList
diff --git a/test/Tests.hs b/test/Tests.hs
new file mode 100644
--- /dev/null
+++ b/test/Tests.hs
@@ -0,0 +1,52 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE OverloadedStrings #-}
+module Main (main) where
+
+#if !MIN_VERSION_base(4,8,0)
+import           Control.Applicative
+#endif
+
+import           Test.QuickCheck.Instances ()
+import           Test.Tasty
+import           Test.Tasty.HUnit
+
+import           Data.Aeson.Compat
+
+import           Orphans ()
+
+main :: IO ()
+main = defaultMain $ testGroup "Tests"
+  [ dotColonMark
+  ]
+
+------------------------------------------------------------------------------
+-- Comparison (.:?) and (.:!)
+------------------------------------------------------------------------------
+
+newtype T1 = T1 (Maybe Int) deriving (Eq, Show)
+newtype T2 = T2 (Maybe Int) deriving (Eq, Show)
+newtype T3 = T3 (Maybe Int) deriving (Eq, Show)
+
+instance FromJSON T1 where parseJSON = fmap T1 . withObject "T1" (.: "value")
+instance FromJSON T2 where parseJSON = fmap T2 . withObject "T2" (.:? "value")
+instance FromJSON T3 where parseJSON = fmap T3 . withObject "T3" (.:! "value")
+
+dotColonMark :: TestTree
+dotColonMark = testGroup "Operators" $ fmap t [
+    assertEqual ".:  not-present" Nothing               (decode ex1 :: Maybe T1)
+  , assertEqual ".:  42"          (Just (T1 (Just 42))) (decode ex2 :: Maybe T1)
+  , assertEqual ".:  null"        (Just (T1 Nothing))   (decode ex3 :: Maybe T1)
+
+  , assertEqual ".:? not-present" (Just (T2 (Nothing))) (decode ex1 :: Maybe T2)
+  , assertEqual ".:? 42"          (Just (T2 (Just 42))) (decode ex2 :: Maybe T2)
+  , assertEqual ".:? null"        (Just (T2 Nothing))   (decode ex3 :: Maybe T2)
+
+  , assertEqual ".:! not-present" (Just (T3 (Nothing))) (decode ex1 :: Maybe T3)
+  , assertEqual ".:! 42"          (Just (T3 (Just 42))) (decode ex2 :: Maybe T3)
+  , assertEqual ".:! null"        Nothing               (decode ex3 :: Maybe T3)
+  ]
+  where ex1 = "{}"
+        ex2 = "{\"value\": 42 }"
+        ex3 = "{\"value\": null }"
+        t   = testCase "-"
+    
