diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,1 @@
+TODO
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2020 Freckle Engineering <engineering@freckle.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,156 @@
+# Hspec Expectations for JSON Values
+
+Comparing JSON `Value`s in Haskell tests comes with some challenges:
+
+- In API responses, additive changes are typically safe and an important way to
+  evolve responses without breaking clients. Therefore, assertions against such
+  responses often want to ignore any unexpected keys in `Object`s (at any
+  depth), as any clients would.
+
+- Order often doesn't matter in API responses either, so it should be possible
+  to assert equality regardless of `Array` ordering (again, at any depth).
+
+- When an assertion fails, showing the difference clearly needs to take the
+  above into account (i.e. it can't show keys you've ignored, or ordering
+  differences you didn't care about), and it has to display things clearly, e.g.
+  as a diff.
+
+This library handles all these things.
+
+## Usage
+
+**NOTE**: this is effectively a distillation of the [Haddocks](#TODO), please
+view them directly for your installed version, to ensure accurate information.
+
+Four expectations exist with the following behaviors:
+
+| Assertion that **fails** on: | extra `Object` keys | wrong `Array` order |
+| ---------------------------- | ------------------- | ------------------- |
+| `shouldBeJson`               | Yes                 | Yes                 |
+| `shouldBeUnorderedJson`      | Yes                 | No                  |
+| `shouldMatchJson`            | No                  | No                  |
+| `shouldMatchOrderedJson`     | No                  | Yes                 |
+
+Each of these, when they fail, print a difference between the objects, where the
+expected-on object has been normalized to avoid indicating any of the
+differences your expectation is ignoring.
+
+### `shouldBeJson`
+
+Passing:
+
+```hs
+catchFailure $
+  [aesonQQ| { "a": true, "b": false } |] `shouldBeJson`
+  [aesonQQ| { "a": true, "b": false } |]
+```
+
+Failing:
+
+```hs
+catchFailure $
+  [aesonQQ| { "a": true, "b": false } |] `shouldBeJson`
+  [aesonQQ| { "a": true, "b": true  } |]
+```
+
+```diff
+   {
+       "a": true,
+---    "b": true
++++    "b": false
+   }
+```
+
+### `shouldBeUnorderedJson`
+
+Passing:
+
+```hs
+catchFailure $
+  [aesonQQ| { "a": [true, false], "b": false } |] `shouldBeUnorderedJson`
+  [aesonQQ| { "a": [false, true], "b": false } |]
+```
+
+Failing:
+
+```hs
+catchFailure $
+  [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldBeUnorderedJson`
+  [aesonQQ| { "a": [false, true], "b": true             } |]
+```
+
+```diff
+   {
+       "a": [
+           false,
+           true
+       ],
+---    "b": true
++++    "b": false,
++++    "c": true
+   }
+```
+
+### `shouldMatchJson`
+
+Passing:
+
+```hs
+catchFailure $
+  [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchJson`
+  [aesonQQ| { "a": [false, true], "b": false            } |]
+```
+
+Failing:
+
+```hs
+catchFailure $
+  [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchJson`
+  [aesonQQ| { "a": [false, true], "b": true             } |]
+```
+
+```diff
+   {
+       "a": [
+           false,
+           true
+       ],
+---    "b": true
++++    "b": false
+   }
+```
+
+### `shouldMatchOrderedJson`
+
+Passing:
+
+```hs
+catchFailure $
+  [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchOrderedJson`
+  [aesonQQ| { "a": [true, false], "b": false            } |]
+```
+
+Failing:
+
+```hs
+catchFailure $
+  [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchOrderedJson`
+  [aesonQQ| { "a": [false, true], "b": true             } |]
+```
+
+```diff
+   {
+       "a": [
+---        false,
+---        true
++++        true,
++++        false
+       ],
+---    "b": true
++++    "b": false
+   }
+```
+
+---
+
+[LICENSE](./LICENSE) | [CHANGELOG](./CHANGELOG.md)
diff --git a/hspec-expectations-json.cabal b/hspec-expectations-json.cabal
new file mode 100644
--- /dev/null
+++ b/hspec-expectations-json.cabal
@@ -0,0 +1,83 @@
+cabal-version: 1.18
+
+-- This file has been generated from package.yaml by hpack version 0.31.2.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: d230360984f4f18d1f942af26155cfa6065ecbc543e330f3f60c37c72ddd6336
+
+name:           hspec-expectations-json
+version:        1.0.0.0
+synopsis:       Hspec expectations for JSON Values
+description:    Hspec expectations for JSON Values
+                .
+                Comparing JSON `Value`s in Haskell tests comes with some challenges:
+                .
+                - In API responses, additive changes are typically safe and an important way
+                  to evolve responses without breaking clients. Therefore, assertions against
+                  such responses often want to ignore any unexpected keys in `Object`s (at any
+                  depth), as any clients would.
+                .
+                - Order often doesn't matter in API responses either, so it should be possible
+                  to assert equality regardless of `Array` ordering (again, at any depth).
+                .
+                - When an assertion fails, showing the difference clearly needs to take the
+                  above into account (i.e. it can't show keys you've ignored, or ordering
+                  differences you didn't care about), and it has to display things clearly,
+                  e.g. as a diff.
+                .
+                This library handles all these things.
+category:       Test
+homepage:       https://github.com/freckle/hspec-expectations-json#readme
+bug-reports:    https://github.com/freckle/hspec-expectations-json/issues
+author:         Freckle Engineering
+maintainer:     engineering@freckle.com
+copyright:      2020 Freckle Education
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+extra-doc-files:
+    CHANGELOG.md
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/freckle/hspec-expectations-json
+
+library
+  exposed-modules:
+      Test.Hspec.Expectations.Json
+      Test.Hspec.Expectations.Json.Internal
+  other-modules:
+      Paths_hspec_expectations_json
+  hs-source-dirs:
+      library
+  default-extensions: BangPatterns DataKinds DeriveAnyClass DeriveFoldable DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable DerivingStrategies FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses NoImplicitPrelude NoMonomorphismRestriction OverloadedStrings RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TypeApplications TypeFamilies
+  build-depends:
+      Diff >=0.4.0 && <0.5
+    , HUnit >=1.6.0.0 && <1.7
+    , aeson >=1.4.7.1 && <1.5
+    , aeson-pretty >=0.8.8 && <0.9
+    , base >=4.11 && <4.14
+    , scientific >=0.3.6.2 && <0.4
+    , text >=1.2.4.0 && <1.3
+    , unordered-containers >=0.2.10.0 && <0.3
+    , vector >=0.12.1.2 && <0.13
+  default-language: Haskell2010
+
+test-suite spec
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Test.Hspec.Expectations.Json.InternalSpec
+      Test.Hspec.Expectations.JsonSpec
+      Paths_hspec_expectations_json
+  hs-source-dirs:
+      tests
+  default-extensions: BangPatterns DataKinds DeriveAnyClass DeriveFoldable DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable DerivingStrategies FlexibleContexts FlexibleInstances GADTs GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses NoImplicitPrelude NoMonomorphismRestriction OverloadedStrings RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TypeApplications TypeFamilies
+  build-depends:
+      aeson-qq
+    , base >=4.11 && <4.14
+    , hspec
+    , hspec-expectations-json
+  default-language: Haskell2010
diff --git a/library/Test/Hspec/Expectations/Json.hs b/library/Test/Hspec/Expectations/Json.hs
new file mode 100644
--- /dev/null
+++ b/library/Test/Hspec/Expectations/Json.hs
@@ -0,0 +1,165 @@
+-- | Expectations on JSON 'Value's
+--
+-- Semantics:
+--
+-- +--------------------------+-------------------+-------------------+
+-- | Assertion that fails on: | extra Object keys | wrong Array order +
+-- +==========================+===================+===================+
+-- | 'shouldBeJson'           | Yes               | Yes               |
+-- +--------------------------+-------------------+-------------------+
+-- | 'shouldBeUnorderedJson'  | Yes               | No                |
+-- +--------------------------+-------------------+-------------------+
+-- | 'shouldMatchJson'        | No                | No                |
+-- +--------------------------+-------------------+-------------------+
+-- | 'shouldMatchOrderedJson' | No                | Yes               |
+-- +--------------------------+-------------------+-------------------+
+--
+module Test.Hspec.Expectations.Json
+  ( shouldBeJson
+  , shouldBeUnorderedJson
+  , shouldMatchJson
+  , shouldMatchOrderedJson
+
+  -- * As predicates
+  -- | These are only created when a specific need arises
+  , matchesJson
+  )
+where
+
+import Prelude
+
+import Data.Aeson
+import Data.Aeson.Encode.Pretty (encodePretty)
+import Data.Text.Lazy (toStrict)
+import Data.Text.Lazy.Encoding (decodeUtf8)
+import GHC.Stack
+import Test.Hspec.Expectations.Json.Internal
+
+-- $setup
+-- >>> :set -XQuasiQuotes
+-- >>> import Data.Aeson.QQ (aesonQQ)
+-- >>> import Test.HUnit.Lang (HUnitFailure(..), formatFailureReason)
+-- >>> import Control.Exception (handle)
+-- >>> let printFailure (HUnitFailure _ r) = putStr $ formatFailureReason r
+-- >>> let catchFailure f = handle printFailure $ f >> putStrLn "<passed>"
+
+-- | Compare two JSON values, with a useful diff
+--
+-- >>> :{
+-- catchFailure $
+--   [aesonQQ| { "a": true, "b": false } |] `shouldBeJson`
+--   [aesonQQ| { "a": true, "b": false } |]
+-- :}
+-- <passed>
+--
+-- >>> :{
+-- catchFailure $
+--   [aesonQQ| { "a": true, "b": false } |] `shouldBeJson`
+--   [aesonQQ| { "a": true, "b": true  } |]
+-- :}
+--    {
+--        "a": true,
+-- ---    "b": true
+-- +++    "b": false
+--    }
+--
+shouldBeJson :: HasCallStack => Value -> Value -> IO ()
+shouldBeJson a b = assertBoolWithDiff (a == b) (toText b) (toText a)
+  where toText = toStrict . decodeUtf8 . encodePretty . normalizeScientific
+
+infix 1 `shouldBeJson`
+
+-- | 'shouldBeJson', ignoring Array ordering
+--
+-- >>> :{
+-- catchFailure $
+--   [aesonQQ| { "a": [true, false], "b": false } |] `shouldBeUnorderedJson`
+--   [aesonQQ| { "a": [false, true], "b": false } |]
+-- :}
+-- <passed>
+--
+-- >>> :{
+-- catchFailure $
+--   [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldBeUnorderedJson`
+--   [aesonQQ| { "a": [false, true], "b": true             } |]
+-- :}
+--    {
+--        "a": [
+--            false,
+--            true
+--        ],
+-- ---    "b": true
+-- +++    "b": false,
+-- +++    "c": true
+--    }
+--
+shouldBeUnorderedJson :: HasCallStack => Value -> Value -> IO ()
+shouldBeUnorderedJson a b = sortJsonArrays a `shouldBeJson` sortJsonArrays b
+
+infix 1 `shouldBeUnorderedJson`
+
+-- | 'shouldBeJson', ignoring extra Object keys or Array ordering
+--
+-- >>> :{
+-- catchFailure $
+--   [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchJson`
+--   [aesonQQ| { "a": [false, true], "b": false            } |]
+-- :}
+-- <passed>
+--
+-- >>> :{
+-- catchFailure $
+--   [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchJson`
+--   [aesonQQ| { "a": [false, true], "b": true             } |]
+-- :}
+--    {
+--        "a": [
+--            false,
+--            true
+--        ],
+-- ---    "b": true
+-- +++    "b": false
+--    }
+--
+shouldMatchJson :: HasCallStack => Value -> Value -> IO ()
+shouldMatchJson sup sub =
+  sortJsonArrays (pruneJson (Superset sup) (Subset sub))
+    `shouldBeJson` sortJsonArrays sub
+
+infix 1 `shouldMatchJson`
+
+-- | Compare JSON values with the same semantics as 'shouldMatchJson'
+matchesJson :: Value -> Value -> Bool
+matchesJson sup sub =
+  sortJsonArrays (pruneJson (Superset sup) (Subset sub)) == sortJsonArrays sub
+
+-- | 'shouldBeJson', ignoring extra Object keys
+--
+-- >>> :{
+-- catchFailure $
+--   [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchOrderedJson`
+--   [aesonQQ| { "a": [true, false], "b": false            } |]
+-- :}
+-- <passed>
+--
+-- >>> :{
+-- catchFailure $
+--   [aesonQQ| { "a": [true, false], "b": false, "c": true } |] `shouldMatchOrderedJson`
+--   [aesonQQ| { "a": [false, true], "b": true             } |]
+-- :}
+--    {
+--        "a": [
+-- ---        false,
+-- ---        true
+-- +++        true,
+-- +++        false
+--        ],
+-- ---    "b": true
+-- +++    "b": false
+--    }
+--
+shouldMatchOrderedJson :: HasCallStack => Value -> Value -> IO ()
+shouldMatchOrderedJson sup sub =
+  pruneJson (Superset sup) (Subset sub) `shouldBeJson` sub
+
+infix 1 `shouldMatchOrderedJson`
diff --git a/library/Test/Hspec/Expectations/Json/Internal.hs b/library/Test/Hspec/Expectations/Json/Internal.hs
new file mode 100644
--- /dev/null
+++ b/library/Test/Hspec/Expectations/Json/Internal.hs
@@ -0,0 +1,139 @@
+-- | Internal building-blocks for JSON 'Value' expectations
+module Test.Hspec.Expectations.Json.Internal
+  (
+  -- * Pretty diff
+    assertBoolWithDiff
+
+  -- * Pruning 'Object's
+  , Superset(..)
+  , Subset(..)
+  , pruneJson
+
+  -- * Sorting 'Array's
+  , Sortable(..)
+  , sortJsonArrays
+  , vectorSortOn
+
+  -- * Dealing with 'Scientific'
+  , normalizeScientific
+  )
+where
+
+import Prelude
+
+import Data.Aeson
+import Data.Algorithm.Diff (PolyDiff(..), getDiff)
+import qualified Data.HashMap.Strict as HashMap
+import Data.List (sortOn)
+import qualified Data.Scientific as Scientific
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Vector (Vector)
+import qualified Data.Vector as V
+import GHC.Stack (HasCallStack)
+import qualified Test.HUnit as HUnit
+
+assertBoolWithDiff :: HasCallStack => Bool -> Text -> Text -> IO ()
+assertBoolWithDiff asserting expected got =
+  flip HUnit.assertBool asserting . unlines . map addSign $ getDiff
+    (lines (T.unpack expected))
+    (lines (T.unpack got))
+ where
+  addSign = \case
+    Both _ s -> "   " ++ s
+    First s -> "---" ++ s
+    Second s -> "+++" ++ s
+
+newtype Superset = Superset Value
+
+newtype Subset = Subset Value
+
+-- | Recursively remove items in the 'Superset' value not present in 'Subset'
+pruneJson :: Superset -> Subset -> Value
+pruneJson (Superset sup) (Subset sub) = case (sup, sub) of
+  (Object a, Object b) -> Object
+    $ HashMap.intersectionWith (\x y -> pruneJson (Superset x) (Subset y)) a b
+
+  -- Pruning elements in Arrays is *extremely* tricky in that it interacts with
+  -- both sorting and matching in what should be a function independent of those
+  -- concerns. There are no good options here, so we make some concessions:
+  --
+  -- 1. It's expected you don't subset differently in different elements of the
+  --    same list. If you have an assertion that needs this behavior, do it
+  --    manually, please
+  --
+  -- 2. It's expected that sorting will be done after pruning, if you intend to
+  --    match irrespective of extra keys or ordering (shouldMatchJson does this)
+  --
+  -- Therefore, we grab the first element from the Subset Array (if present) and
+  -- prune all elements of the Superset Array using it. This ensures that
+  -- different sorts or length in the Superset side are preserved, but we
+  -- are still able to prune *before* the sorting required for matching, which
+  -- is important.
+  --
+  -- Other options such as sort-before-prune, or pair-wise pruning (with align
+  -- or zip) all correctly handle some cases but not all. And most importantly,
+  -- the cases those options don't handle come out as confusing assertion
+  -- failures.
+  --
+  (Array a, Array b) -> Array $ case b V.!? 0 of
+    Nothing -> a
+    Just y -> (\x -> pruneJson (Superset x) (Subset y)) <$> a
+
+  (x, _) -> x
+
+newtype Sortable = Sortable Value
+  deriving newtype Eq
+
+instance Ord Sortable where
+  Sortable a `compare` Sortable b = case (a, b) of
+    (String x, String y) -> x `compare` y
+    (Number x, Number y) -> x `compare` y
+    (Bool x, Bool y) -> x `compare` y
+    (Null, Null) -> EQ -- forgive me
+    (Array x, Array y) -> V.map Sortable x `compare` V.map Sortable y
+    (Object x, Object y) ->
+      HashMap.map Sortable x `compare` HashMap.map Sortable y
+    (x, y) -> arbitraryRank x `compare` arbitraryRank y
+   where
+    arbitraryRank :: Value -> Int
+    arbitraryRank = \case
+      Object{} -> 5
+      Array{} -> 4
+      String{} -> 3
+      Number{} -> 2
+      Bool{} -> 1
+      Null -> 0
+
+sortJsonArrays :: Value -> Value
+sortJsonArrays = \case
+  Array v -> Array $ vectorSortOn Sortable $ sortJsonArrays <$> v
+  Object hm -> Object $ HashMap.map sortJsonArrays hm
+  x@String{} -> x
+  x@Number{} -> x
+  x@Bool{} -> x
+  x@Null{} -> x
+
+vectorSortOn :: Ord b => (a -> b) -> Vector a -> Vector a
+vectorSortOn f v = v V.// zip [0 ..] sorted
+  where sorted = sortOn f $ V.toList v
+
+-- | Normalize all 'Number' values to 'Double' precision
+--
+-- Internally, @1@ and @1.0@ are represented as different values of the
+-- 'Scientific' data type. These will compare equally, but if there is some
+-- /other/ difference that fails the assertion, they will render as a difference
+-- in the message, confusing the reader.
+--
+-- This sends them through an 'id' function as 'Double', which will make either
+-- print as @1.0@ consistently.
+--
+normalizeScientific :: Value -> Value
+normalizeScientific = \case
+  Object hm -> Object $ HashMap.map normalizeScientific hm
+  Array vs -> Array $ normalizeScientific <$> vs
+  x@String{} -> x
+  Number sci ->
+    Number $ Scientific.fromFloatDigits @Double $ Scientific.toRealFloat sci
+  x@Bool{} -> x
+  x@Null -> x
diff --git a/tests/Spec.hs b/tests/Spec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Spec.hs
@@ -0,0 +1,2 @@
+{-# OPTIONS_GHC -fno-warn-missing-export-lists #-}
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/tests/Test/Hspec/Expectations/Json/InternalSpec.hs b/tests/Test/Hspec/Expectations/Json/InternalSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Test/Hspec/Expectations/Json/InternalSpec.hs
@@ -0,0 +1,100 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Test.Hspec.Expectations.Json.InternalSpec
+  ( spec
+  )
+where
+
+import Prelude
+
+import Data.Aeson.QQ
+import Test.Hspec
+import Test.Hspec.Expectations.Json (shouldBeJson)
+import Test.Hspec.Expectations.Json.Internal
+
+spec :: Spec
+spec = do
+  describe "pruneJson" $ do
+    it "prunes object keys from Superset not present in Subset" $ do
+      let
+        sup = Superset [aesonQQ|{ "foo": "foo", "baz": "baz" }|]
+        sub = Subset [aesonQQ|{ "foo": "bar" } |]
+
+      pruneJson sup sub `shouldBeJson` [aesonQQ|{ "foo": "foo" }|]
+
+    it "prunes object keys recursively Superset not present in Subset" $ do
+      let
+        sup = Superset [aesonQQ|
+          { "foo": { "bar": "bar" , "baz": "baz" }
+          , "bat": "bat"
+          }
+        |]
+
+        sub = Subset [aesonQQ|
+          { "foo": { "bar": "baz" }
+          }
+        |]
+
+      pruneJson sup sub `shouldBeJson` [aesonQQ|{ "foo": { "bar": "bar" } }|]
+
+    it "prunes objects within Arrays" $ do
+      let
+        sup = Superset [aesonQQ|
+          [ { "foo": "bar", "quix": "cats" }
+          , { "foo": "bats", "quix": "baz" }
+          ]
+        |]
+
+        sub = Subset [aesonQQ|
+          [ { "foo": "zap" }
+          , { "foo": "zop" }
+          ]
+        |]
+
+      pruneJson sup sub `shouldBeJson` [aesonQQ|
+       [ { "foo": "bar" }
+       , { "foo": "bats" }
+       ]
+      |]
+
+    it "handles mismatching types" $ do
+      let
+        sup = Superset [aesonQQ|
+          { "foo": { "bar": 1, "baz": "baz" }
+          , "bat": { "bat": 1 }
+          }
+        |]
+
+        sub = Subset [aesonQQ|
+          { "foo": { "bar": "baz" }
+          , "bat": "bat"
+          }
+        |]
+
+      pruneJson sup sub `shouldBeJson` [aesonQQ|
+        { "foo": { "bar": 1 }
+        , "bat": { "bat": 1 }
+        }
+      |]
+
+  describe "sortJsonArrays" $ do
+    it "sorts arrays" $ do
+      let
+        unsorted = [aesonQQ|["number_facts", "number_basics"]|]
+        sorted = [aesonQQ|["number_basics", "number_facts"]|]
+
+      sortJsonArrays unsorted `shouldBeJson` sorted
+
+    it "sorts arrays in object keys" $ do
+      let
+        unsorted = [aesonQQ|{ "x": ["number_facts", "number_basics"] }|]
+        sorted = [aesonQQ|{ "x": ["number_basics", "number_facts"] }|]
+
+      sortJsonArrays unsorted `shouldBeJson` sorted
+
+    it "works on arrays of nested arrays" $ do
+      let
+        unsorted = [aesonQQ|[{ "x": ["number_facts", "number_basics"] }]|]
+        sorted = [aesonQQ|[{ "x": ["number_basics", "number_facts"] }]|]
+
+      sortJsonArrays unsorted `shouldBeJson` sorted
diff --git a/tests/Test/Hspec/Expectations/JsonSpec.hs b/tests/Test/Hspec/Expectations/JsonSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Test/Hspec/Expectations/JsonSpec.hs
@@ -0,0 +1,113 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Test.Hspec.Expectations.JsonSpec
+  ( spec
+  )
+where
+
+import Prelude
+
+import Data.Aeson.QQ
+import Test.Hspec
+import Test.Hspec.Expectations.Json
+
+spec :: Spec
+spec = do
+  describe "shouldMatchJson" $ do
+    it "passes regardless of array order" $ do
+      let
+        a = [aesonQQ|[{ "foo": 1 }, { "foo": 0 }]|]
+        b = [aesonQQ|[{ "foo": 0 }, { "foo": 1 }]|]
+
+      a `shouldMatchJson` b
+
+    it "passes regardless of array order at depth" $ do
+      let
+        a = [aesonQQ|{ "a": [{ "foo": 1 }, { "foo": 0 }] }|]
+        b = [aesonQQ|{ "a": [{ "foo": 0 }, { "foo": 1 }] }|]
+
+      a `shouldMatchJson` b
+
+    it "passes regardless of extra keys" $ do
+      let
+        a = [aesonQQ|{ "foo": "bar", "baz": "bat" }|]
+        b = [aesonQQ|{ "foo": "bar" }|]
+
+      a `shouldMatchJson` b
+
+    it "passes regardless of extra keys at depth" $ do
+      let
+        a = [aesonQQ|{ "a": { "foo": "bar", "baz": "bat" } }|]
+        b = [aesonQQ|{ "a": { "foo": "bar" } }|]
+
+      a `shouldMatchJson` b
+
+    it "matches pruned and unsorted array elements" $ do
+      let
+        sup1 = [aesonQQ|{ "studentId": 1, "x": true }|]
+        sub1 = [aesonQQ|{ "studentId": 1 }|]
+        sup2 = [aesonQQ|{ "studentId": 2, "x": true }|]
+        sub2 = [aesonQQ|{ "studentId": 2 }|]
+        sup3 = [aesonQQ|{ "studentId": 3, "x": true }|]
+        sub3 = [aesonQQ|{ "studentId": 3 }|]
+        sup4 = [aesonQQ|{ "studentId": 4, "x": true }|]
+        sub4 = [aesonQQ|{ "studentId": 4 }|]
+
+        a = [aesonQQ|
+          [ { "stats": [#{sup3}] }
+          , { "stats": [#{sup3}, #{sup1}] }
+          , { "stats": [#{sup4}, #{sup2}] }
+          , { "stats": [#{sup2}] }
+          , { "stats": [#{sup4}] }
+          , { "stats": [#{sup4}] }
+          ]
+        |]
+
+        b = [aesonQQ|
+          [ { "stats": [#{sub1}, #{sub3}] }
+          , { "stats": [#{sub3}] }
+          , { "stats": [#{sub2}, #{sub4}] }
+          , { "stats": [#{sub2}] }
+          , { "stats": [#{sub4}] }
+          , { "stats": [#{sub4}] }
+          ]
+        |]
+
+      a `shouldMatchJson` b
+
+    it "handles cases where sorting differs after pruning" $ do
+      let
+        a = [aesonQQ|
+          [ { "shortName": "B"
+            , "subSkills":
+              [ { "shortName": "1"
+                , "uspId": "68fa57ddbc1e9aa332f7c88884e5c40e"
+                }
+              ]
+            }
+          , { "shortName": "A"
+            , "subSkills":
+                 [ { "shortName": "a"
+                   , "uspId": "71839723561ba1a49cf2a789dbe50302"
+                   }
+                 , { "shortName": "b"
+                   , "uspId": "4096d2cfebcab73438971ae2304544ee"
+                   }
+                 ]
+            }
+          ]
+        |]
+        b = [aesonQQ|
+          [ { "subSkills":
+              [ { "uspId": "71839723561ba1a49cf2a789dbe50302" }
+              , { "uspId": "4096d2cfebcab73438971ae2304544ee" }
+              ]
+            }
+          , { "subSkills":
+              [ { "uspId": "68fa57ddbc1e9aa332f7c88884e5c40e" }
+              ]
+            }
+          ]
+        |]
+
+      a `shouldMatchJson` b
