diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+aeson-diff 1.0
+
+    * Initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2014, Thomas Sutton
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+
+2. 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.
+
+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,85 @@
+Aeson Diff
+==========
+
+[![Build Status][badge]][status]
+
+This is a small library for working with changes to JSON documents. It includes
+a library and two executables in the style of diff(1) and patch(1).
+
+Installing
+----------
+
+The `aeson-diff` package is written in Haskell and can be installed using the
+Cabal package management tool.
+
+For the command-line tools only, I recommend using Cabal's sandbox
+functionality to avoid installing the associated libraries globally on your
+system. This approach might look something like the following:
+
+````bash
+cd aeson-diff/
+cabal sandbox init
+cabal install --dependencies-only
+cabal build
+sudo mkdir -p /usr/local/bin
+sudo cp dist/build/json-*/json-{diff,patch} /usr/local/bin/
+````
+
+If you want to use the library, use Cabal in a sandbox or not according to your
+preference.
+
+````bash
+cabal sandbox init
+cabal sandbox install ~/Downloads/aeson-diff/
+````
+
+Usage
+-----
+
+### json-diff command
+
+The `json-diff` command compares two JSON documents and extracts a patch
+describing the differences between the first document and the second.
+
+````
+Usage: json-diff [-j|--json] [-o|--output OUTPUT] FROM TO
+Generate a patch between two JSON documents.
+
+Available options:
+    -h,--help                Show this help text
+    -j,--json                Output patch in JSON.
+````
+
+### json-patch command
+
+The `json-patch` command applies a patch describing changes to be made to
+a JSON document.
+
+````
+Usage: json-patch [-j|--json] [-o|--output OUTPUT] PATCH FROM
+Generate a patch between two JSON documents.
+
+Available options:
+  -h,--help                Show this help text
+  -j,--json                Patch is in JSON format.
+  -o,--output OUTPUT       Destination for patched JSON.
+  PATCH                    Patch to apply.
+  FROM                     JSON file to patch.
+````
+
+### aeson-diff library
+
+The `aeson-diff` library exports as single module: `Data.Aeson.Diff`. This
+exports `diff` and `patch` functions which do exactly what might be expected:
+
+- `diff :: Value -> Value -> Patch` examines source and target JSON `Value`s
+and constructs a new `Patch` describing the changes.
+
+- `patch :: Patch -> Value -> Value` applies the changes in a `Patch` to a JSON
+`Value`.
+
+For more complete information, see the [documentation][docs].
+
+[badge]: https://travis-ci.org/thsutton/aeson-diff.svg?branch=master
+[status]: https://travis-ci.org/thsutton/aeson-diff
+[docs]: https://hackage.haskell.org/package/aeson-diff/docs/Data-Aeson-Diff.html
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-diff.cabal b/aeson-diff.cabal
new file mode 100644
--- /dev/null
+++ b/aeson-diff.cabal
@@ -0,0 +1,73 @@
+name:                aeson-diff
+version:             0.1.1.1
+synopsis:            Extract and apply patches to JSON documents.
+description:
+  .
+  This is a small library for working with changes to JSON documents. It
+  includes a library and two command-line executables in the style of the
+  diff(1) and patch(1) commands available on many systems.
+  .
+homepage:            https://github.com/thsutton/aeson-diff
+license:             BSD3
+license-file:        LICENSE
+author:              Thomas Sutton
+maintainer:          me@thomas-sutton.id.au
+copyright:           (c) 2015 Thomas Sutton and others.
+category:            JSON, Web, Algorithms
+build-type:          Simple
+extra-source-files:  README.md, CHANGELOG.md
+cabal-version:       >=1.10
+
+source-repository     HEAD
+  type: git
+  location: https://github.com/thsutton/aeson-diff
+
+library
+  default-language:    Haskell2010
+  hs-source-dirs:      lib
+  exposed-modules:     Data.Aeson.Diff
+  build-depends:       base >=4.7 && <4.9
+                     , aeson >=0.8 && <0.9
+                     , bytestring
+                     , edit-distance-vector >=1.0 && <2.0
+                     , hashable
+                     , mtl
+                     , scientific
+                     , text
+                     , unordered-containers
+                     , vector
+
+executable             json-diff
+  default-language:    Haskell2010
+  hs-source-dirs:      src
+  main-is:             diff.hs
+  build-depends:       base
+                     , aeson
+                     , aeson-diff
+                     , bytestring
+                     , optparse-applicative >=0.11 && < 0.12
+                     , text
+
+executable             json-patch
+  default-language:    Haskell2010
+  hs-source-dirs:      src
+  main-is:             patch.hs
+  build-depends:       base
+                     , aeson
+                     , aeson-diff
+                     , bytestring
+                     , optparse-applicative >=0.11 && < 0.12
+
+test-suite             properties
+  default-language:    Haskell2010
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             properties.hs
+  build-depends:       base
+                     , QuickCheck
+                     , aeson
+                     , aeson-diff
+                     , quickcheck-instances
+                     , text
+                     , unordered-containers
+                     , vector
diff --git a/lib/Data/Aeson/Diff.hs b/lib/Data/Aeson/Diff.hs
new file mode 100644
--- /dev/null
+++ b/lib/Data/Aeson/Diff.hs
@@ -0,0 +1,343 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+
+-- | Description: Extract and apply patches on JSON documents.
+--
+-- This module implements data types and operations to represent the
+-- differences between JSON documents (i.e. a patch), to compare JSON documents
+-- and extract such a patch, and to apply such a patch to a JSON document.
+module Data.Aeson.Diff (
+    -- * Patches
+    Patch,
+    patchOperations,
+    Path,
+    Key(..),
+    Operation(..),
+
+    -- * Functions
+    diff,
+    patch,
+    applyOperation,
+
+    formatPatch,
+) where
+
+import Control.Applicative
+import Control.Monad
+import Control.Monad.Error.Class
+import Data.Aeson
+import Data.Hashable
+import Data.HashMap.Strict (HashMap)
+import qualified Data.HashMap.Strict as HM
+import Data.Maybe
+import Data.Monoid
+import Data.Scientific
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Vector (Vector)
+import qualified Data.Vector as V
+
+import Data.Vector.Distance
+
+-- | Describes the changes between two JSON documents.
+data Patch = Patch
+  { patchOperations :: [Operation] }
+  deriving (Eq)
+
+instance Show Patch where
+    show = T.unpack . formatPatch
+
+instance Monoid Patch where
+    mempty = Patch []
+    mappend (Patch p1) (Patch p2) = Patch $ p1 <> p2
+
+instance ToJSON Patch where
+    toJSON (Patch ops) = object [ "patch" .= ops ]
+
+instance FromJSON Patch where
+    parseJSON (Object v) = Patch <$> v .: "patch"
+    parseJSON _ = fail "Patch must be a JSON object."
+
+instance ToJSON Operation where
+    toJSON (Ins p v) = object
+        [ ("change", "insert")
+        , "path"  .= p
+        , "value" .= v
+        ]
+    toJSON (Del p v) = object
+        [ ("change", "delete")
+        , "path" .= p
+        , "value" .= v
+        ]
+
+instance FromJSON Operation where
+    parseJSON (Object v)
+        =  (fixed v "change" (String "insert") *>
+            (Ins <$> v .: "path" <*> v .: "value"))
+        <> (fixed v "change" (String "delete") *>
+            (Del <$> v .: "path" <*> v .: "value"))
+      where
+        fixed o n val = do
+            v' <- o .: n
+            unless (v' == val) . fail . T.unpack $ "Cannot find " <> n <> "."
+            return v'
+    parseJSON _ = fail "Operation must be a JSON object."
+
+-- | Descripts an atomic change to a JSON document.
+data Operation
+    = Ins { changePath :: Path, changeValue :: Value }
+    -- ^ Insert a value at a location.
+    | Del { changePath :: Path, changeValue :: Value }
+    -- ^ Delete the value at a location.
+  deriving (Eq, Show)
+
+-- | A path through a JSON document is a possibly empty sequence of 'Key's.
+type Path = [Key]
+
+-- | Traverse a single layer of a JSON document.
+data Key
+    = OKey Text
+    | AKey Int
+  deriving (Eq, Ord, Show)
+
+instance ToJSON Key where
+    toJSON (OKey t) = String t
+    toJSON (AKey a) = Number . fromInteger . toInteger $ a
+
+instance FromJSON Key where
+    parseJSON (String t) = return $ OKey t
+    parseJSON (Number n) =
+        case toBoundedInteger n of
+            Nothing -> fail "A numeric key must be a positive whole number."
+            Just n' -> return $ AKey n'
+    parseJSON _ = fail "A key element must be a number or a string."
+
+-- | Modify the 'Path' of an 'Operation'.
+modifyPath :: (Path -> Path) -> Operation -> Operation
+modifyPath path op = op { changePath = path (changePath op) }
+
+-- * Atomic patches
+
+-- | Construct a patch with a single 'Ins' operation.
+ins :: Path -> Value -> Patch
+ins p v = Patch [Ins p v]
+
+-- | Construct a patch with a single 'Del' operation.
+del :: Path -> Value -> Patch
+del p v = Patch [Del p v]
+
+-- | Construct a patch which changes a single value.
+ch :: Path -> Value -> Value -> Patch
+ch p v1 v2 = del p v1 <> ins p v2
+
+-- * Operations
+
+-- | Compare two JSON documents and generate a patch describing the differences.
+diff
+    :: Value
+    -> Value
+    -> Patch
+diff = worker []
+  where
+    check :: Monoid m => Bool -> m -> m
+    check b v = if b then mempty else v
+
+    worker :: Path -> Value -> Value -> Patch
+    worker p v1 v2 = case (v1, v2) of
+        -- For atomic values of the same type, emit changes iff they differ.
+        (Null,      Null)      -> mempty
+        (Bool b1,   Bool b2)   -> check (b1 == b2) $ ch p v1 v2
+        (Number n1, Number n2) -> check (n1 == n2) $ ch p v1 v2
+        (String s1, String s2) -> check (s1 == s2) $ ch p v1 v2
+
+        -- For structured values of the same type, walk them.
+        (Array a1,  Array a2)  -> check (a1 == a2) $ workArray p a1 a2
+        (Object o1, Object o2) -> check (o1 == o2) $ workObject p o1 o2
+
+        -- For values of different types, delete v1 and insert v2.
+        _                      -> del p v1 <> ins p v2
+
+    -- Walk the keys in two objects, producing a 'Patch'.
+    workObject :: Path -> Object -> Object -> Patch
+    workObject p o1 o2 =
+        let k1 = HM.keys o1
+            k2 = HM.keys o2
+            -- Deletions
+            del_keys = filter (not . (`elem` k2)) k1
+            deletions = Patch $ fmap
+                (\k -> Del (p <> [OKey k]) . fromJust $ HM.lookup k o1)
+                del_keys
+            -- Insertions
+            ins_keys = filter (not . (`elem` k1)) k2
+            insertions = Patch $ fmap
+                (\k -> Ins (p <> [OKey k]) . fromJust $ HM.lookup k o2)
+                ins_keys
+            -- Changes
+            chg_keys = filter (`elem` k2) k1
+            changes = fmap
+                (\k -> worker (p <> [OKey k])
+                    (fromJust $ HM.lookup k o1)
+                    (fromJust $ HM.lookup k o2))
+                chg_keys
+        in deletions <> insertions <> mconcat changes
+
+    -- Use an adaption of the Wagner-Fischer algorithm to find the shortest
+    -- sequence of changes between two JSON arrays.
+    workArray :: Path -> Array -> Array -> Patch
+    workArray path ss tt = Patch . snd . fmap concat $ leastChanges params ss tt
+      where
+        params :: Params Value [Operation] (Sum Int)
+        params = Params{..}
+        equivalent = (==)
+        delete i v = [Del (path <> [AKey i]) v]
+        insert i v = [Ins (path <> [AKey i]) v]
+        substitute i v v' =
+            let p = path <> [AKey i]
+                Patch ops = diff v v'
+            in fmap (modifyPath (p <>)) ops
+        cost = Sum . sum . fmap (valueSize . changeValue)
+        positionOffset = sum . fmap pos
+        pos Del{} = 0
+        pos Ins{} = 1
+
+-- | Apply a patch to a JSON document.
+patch
+    :: Patch
+    -> Value
+    -> Value
+patch (Patch []) val = val
+patch (Patch ops) val = foldl (flip applyOperation) val ops
+
+-- | Apply an 'Operation' to a 'Value'.
+applyOperation
+    :: Operation
+    -> Value
+    -> Value
+applyOperation op j = case op of
+    Ins path v' -> insert path v' j
+    Del path v' -> delete path v' j
+  where
+    insert :: Path -> Value -> Value -> Value
+    insert [] v' _ = v'
+    -- Apply a local change.
+    insert [AKey i] v' (Array  v) = Array  $ vInsert   i v' v
+    insert [OKey n] v' (Object m) = Object $ HM.insert n v' m
+    -- Traverse for deeper changes.
+    insert (AKey i : path) v' (Array v) = Array $ vModify i
+        (Just . insert path v' . fromMaybe (Array mempty)) v
+    insert (OKey n : path) v' (Object m) = Object $ hmModify n
+        (Just . insert path v' . fromMaybe (Object mempty)) m
+    -- Hey I just met you / And this is crazy
+    -- But here's my data / Discard it maybe?
+    --
+    -- Type mismatch; let's throw away the thing we weren't expecting!
+    insert (AKey _ : path) v' v = Array $ V.singleton (insert path v' v)
+    insert (OKey n : path) v' v = Object $ HM.singleton n (insert path v' v)
+
+    delete :: Path -> Value -> Value -> Value
+    -- Apply a local change.
+    --
+    -- TODO(thsutton) We might want to check that the item addressed by the key
+    -- is similar to @_v'@.
+    delete [AKey i] _v' (Array  v) = Array  $ vDelete   i v
+    delete [OKey n] _v' (Object m) = Object $ HM.delete n m
+    -- Traverse for deeper changes.
+    delete (AKey i : rest) v' (Array v) = Array $ vModify i
+        (fmap (delete rest v')) v
+    delete (OKey n : rest) v' (Object m) = Object $ hmModify n
+        (fmap (delete rest v')) m
+    -- Type mismatch: clearly the thing we're deleting isn't here.
+    delete _  _v' v = v
+
+-- * Formatting patches
+
+-- | Format a 'Patch'.
+formatPatch :: Patch -> Text
+formatPatch (Patch ops) = T.unlines
+    $ fmap formatOp ops
+  where
+    formatKey (OKey t) = "." <> t
+    formatKey (AKey i) = "[" <> (T.pack . show $ i) <> "]"
+    formatPath :: [Key] -> Text
+    formatPath p = "@" <> (T.concat . fmap formatKey $ p)
+    formatOp :: Operation -> Text
+    formatValue :: Value -> Text
+    formatValue v = case v of
+        String t -> t
+        Number s -> T.pack . show $ s
+        Bool b -> T.pack . show $ b
+        Null -> "Null"
+        _ -> ":-("
+    formatOp (Ins k v) = formatPath k <> "\n" <> "+" <> formatValue v
+    formatOp (Del k v) = formatPath k <> "\n" <> "-" <> formatValue v
+
+-- | Parse a 'Patch'.
+parsePatch :: Text -> Either Text Patch
+parsePatch _t = throwError "Cannot parse"
+
+-- * Utilities
+--
+-- $ These are some utility functions used in the functions defined above. Mostly
+-- they just fill gaps in the APIs of the "Data.Vector" and "Data.HashMap.Strict"
+-- modules.
+
+-- | Estimate the size of a JSON 'Value'.
+--
+-- This is used in the diff cost metric function.
+valueSize :: Value -> Int
+valueSize val = case val of
+    Object o -> sum . fmap valueSize . HM.elems $ o
+    Array  a -> V.sum $ V.map valueSize a
+    _        -> 1
+
+-- | Delete an element in a vector.
+vDelete :: Int -> Vector a -> Vector a
+vDelete i v =
+    let l = V.length v
+    in V.slice 0 i v <> V.slice (i + 1) (l - i - 1) v
+
+-- | Insert an element into a vector.
+vInsert :: Int -> a -> Vector a -> Vector a
+vInsert i a v
+    | i <= 0          = V.cons a v
+    | V.length v <= i = V.snoc v a
+    | otherwise       = V.slice 0 i v
+                      <> V.singleton a
+                      <> V.slice i (V.length v - i) v
+
+-- | Modify the element at an index in a 'Vector'.
+--
+-- The function is passed the value at index @i@, or 'Nothing' if there is no
+-- such element. The function should return 'Nothing' if it wants to have no
+-- value corresponding to the index, or 'Just' if it wants a value.
+--
+-- Depending on the vector and the function, we will either:
+--
+-- - leave the vector unchanged;
+-- - delete an existing element;
+-- - insert a new element; or
+-- - replace an existing element.
+vModify :: Int -> (Maybe a -> Maybe a) -> Vector a -> Vector a
+vModify i f v =
+    let a = v V.!? i
+        a' = f a
+    in case (a, a') of
+        (Nothing, Nothing) -> v
+        (Just _,  Nothing) -> vDelete i v
+        (Nothing, Just n ) -> vInsert i n v
+        (Just _,  Just n ) -> V.update v (V.singleton (i, n))
+
+-- | Modify the value associated with a key in a 'HashMap'.
+--
+-- The function is passed the value defined for @k@, or 'Nothing'. If the
+-- function returns 'Nothing', the key and value are deleted from the map;
+-- otherwise the value replaces the existing value in the returned map.
+hmModify
+    :: (Eq k, Hashable k)
+    => k
+    -> (Maybe v -> Maybe v)
+    -> HashMap k v
+    -> HashMap k v
+hmModify k f m = case f (HM.lookup k m) of
+    Nothing -> HM.delete k m
+    Just v  -> HM.insert k v m
diff --git a/src/diff.hs b/src/diff.hs
new file mode 100644
--- /dev/null
+++ b/src/diff.hs
@@ -0,0 +1,107 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+
+module Main where
+
+import Control.Applicative
+import Control.Exception
+import Data.Aeson
+import Data.Aeson.Diff
+import qualified Data.ByteString.Char8 as BS
+import qualified Data.ByteString.Lazy as BSL
+import Data.Monoid
+import qualified Data.Text.IO as T
+import Options.Applicative
+import Options.Applicative.Types
+import System.IO
+
+type File = Maybe FilePath
+
+-- | Command-line options.
+data Options = Options
+    { optionJSON :: Bool
+    , optionOut  :: File
+    , optionFrom :: File
+    , optionTo   :: File
+    }
+
+data Configuration = Configuration
+    { cfgJSON :: Bool
+    , cfgOut  :: Handle
+    , cfgFrom :: Handle
+    , cfgTo   :: Handle
+    }
+
+optionParser :: Parser Options
+optionParser = Options
+    <$> switch
+        (  long "json"
+        <> short 'j'
+        <> help "Output patch in JSON."
+        )
+    <*> option fileP
+        (  long "output"
+        <> short 'o'
+        <> metavar "OUTPUT"
+        <> value Nothing
+        )
+    <*> argument fileP
+        (  metavar "FROM"
+        )
+    <*> argument fileP
+        (  metavar "TO"
+        )
+  where
+    fileP = do
+        s <- readerAsk
+        return $ case s of
+            "-" -> Nothing
+            _ -> Just s
+
+jsonFile :: Handle -> IO Value
+jsonFile fp = do
+    s <- BS.hGetContents fp
+    case decode (BSL.fromStrict s)of
+        Nothing -> error "Could not parse as JSON"
+        Just v -> return v
+
+run :: Options -> IO ()
+run opt = bracket (load opt) close process
+  where
+    openr :: Maybe FilePath -> IO Handle
+    openr Nothing = return stdin
+    openr (Just p) = openFile p ReadMode
+
+    openw :: Maybe FilePath -> IO Handle
+    openw Nothing = return stdout
+    openw (Just p) = openFile p WriteMode
+
+    load :: Options -> IO Configuration
+    load Options{..} =
+        Configuration
+            <$> pure  optionJSON
+            <*> openw optionOut
+            <*> openr optionFrom
+            <*> openr optionTo
+
+    close :: Configuration -> IO ()
+    close Configuration{..} = do
+        hClose cfgOut
+        hClose cfgFrom
+        hClose cfgTo
+
+process :: Configuration -> IO ()
+process Configuration{..} = do
+    json_from <- jsonFile cfgFrom
+    json_to <- jsonFile cfgTo
+    let p = diff json_from json_to
+    if cfgJSON
+        then BS.hPutStrLn cfgOut . BSL.toStrict . encode $ p
+        else T.hPutStrLn cfgOut . formatPatch $ p
+
+main :: IO ()
+main = execParser opts >>= run
+  where
+    opts = info (helper <*> optionParser)
+     (  fullDesc
+     <> progDesc "Generate a patch between two JSON documents.")
diff --git a/src/patch.hs b/src/patch.hs
new file mode 100644
--- /dev/null
+++ b/src/patch.hs
@@ -0,0 +1,110 @@
+{-# LANGUAGE RecordWildCards #-}
+
+module Main where
+
+import Control.Applicative
+import Control.Exception
+import Data.Aeson
+import Data.Aeson.Diff
+import qualified Data.ByteString.Char8 as BS
+import qualified Data.ByteString.Lazy as BSL
+import Data.Monoid
+import Options.Applicative hiding (Success)
+import Options.Applicative.Types hiding (Success)
+import System.IO
+
+type File = Maybe FilePath
+
+-- | Command-line options.
+data Options = Options
+    { optionJSON  :: Bool -- ^ Patch is JSON?
+    , optionOut   :: File -- ^ JSON destination
+    , optionPatch :: File -- ^ Patch input
+    , optionFrom  :: File -- ^ JSON source
+    }
+
+data Configuration = Configuration
+    { cfgJSON  :: Bool
+    , cfgOut   :: Handle
+    , cfgPatch :: Handle
+    , cfgFrom  :: Handle
+    }
+
+optionParser :: Parser Options
+optionParser = Options
+    <$> switch
+        (  long "json"
+        <> short 'j'
+        <> help "Patch is in JSON format."
+        )
+    <*> option fileP
+        (  long "output"
+        <> short 'o'
+        <> metavar "OUTPUT"
+        <> help "Destination for patched JSON."
+        <> value Nothing
+        )
+    <*> argument fileP
+        (  metavar "PATCH"
+        <> help "Patch to apply."
+        )
+    <*> argument fileP
+        (  metavar "FROM"
+        <> help "JSON file to patch."
+        )
+  where
+    fileP = do
+        s <- readerAsk
+        return $ case s of
+            "-" -> Nothing
+            _ -> Just s
+
+jsonRead :: Handle -> IO Value
+jsonRead fp = do
+    s <- BS.hGetContents fp
+    case decode (BSL.fromStrict s)of
+        Nothing -> error "Could not parse as JSON"
+        Just v -> return v
+
+run :: Options -> IO ()
+run opt = bracket (load opt) close process
+  where
+    openr :: Maybe FilePath -> IO Handle
+    openr Nothing = return stdin
+    openr (Just p) = openFile p ReadMode
+
+    openw :: Maybe FilePath -> IO Handle
+    openw Nothing = return stdout
+    openw (Just p) = openFile p WriteMode
+
+    load :: Options -> IO Configuration
+    load Options{..} =
+        Configuration
+            <$> pure  optionJSON
+            <*> openw optionOut
+            <*> openr optionPatch
+            <*> openr optionFrom
+
+    close :: Configuration -> IO ()
+    close Configuration{..} = do
+        hClose cfgPatch
+        hClose cfgFrom
+        hClose cfgOut
+
+process :: Configuration -> IO ()
+process Configuration{..} = do
+    json_patch <- if cfgJSON
+        then jsonRead cfgPatch
+        else error "Patch must be in JSON format. Sorry :-("
+    json_from <- jsonRead cfgFrom
+    case fromJSON json_patch of
+        Error e -> error e
+        Success p -> BS.hPutStrLn cfgOut . BSL.toStrict . encode
+                $ patch p json_from
+
+main :: IO ()
+main = execParser opts >>= run
+  where
+    opts = info (helper <*> optionParser)
+     (  fullDesc
+     <> progDesc "Generate a patch between two JSON documents.")
diff --git a/test/properties.hs b/test/properties.hs
new file mode 100644
--- /dev/null
+++ b/test/properties.hs
@@ -0,0 +1,67 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell   #-}
+
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module Main where
+
+import Control.Monad
+import Data.Aeson
+import Data.HashMap.Strict (HashMap)
+import Data.Monoid
+import Data.Text (Text)
+import qualified Data.Vector as V
+import System.Exit
+import Test.QuickCheck
+import Test.QuickCheck.Instances ()
+
+import Data.Aeson.Diff
+
+instance Arbitrary Value where
+    arbitrary = elements
+        [ Null
+        , Bool True
+        , Number 123
+        , String "boo"
+        , Array . V.fromList $ [String "123", Number 123]
+        ]
+
+-- | Patch extracted from identical documents should be mempty.
+prop_diff_id
+    :: Value
+    -> Bool
+prop_diff_id v = diff v v == mempty
+
+-- | Extracting and applying a patch is an identity.
+prop_diff_apply
+    :: Value
+    -> Value
+    -> Bool
+prop_diff_apply f t = t == patch (diff f t) f
+
+-- | Extract and apply a patch (specialised to JSON arrays).
+prop_diff_arrays
+    :: [Value]
+    -> [Value]
+    -> Bool
+prop_diff_arrays v1 v2 = prop_diff_apply (Array . V.fromList $ v1) (Array . V.fromList $ v2)
+
+-- | Extract and apply a patch (specialised to JSON objects).
+prop_diff_objects
+    :: HashMap Text Value
+    -> HashMap Text Value
+    -> Bool
+prop_diff_objects m1 m2 = prop_diff_apply (Object m1) (Object m2)
+
+--
+-- Use Template Haskell to automatically run all of the properties above.
+--
+
+return []
+runTests :: IO Bool
+runTests = $quickCheckAll
+
+main :: IO ()
+main = do
+    result <- runTests
+    unless result exitFailure
