diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
 ## [_Unreleased_](https://github.com/freckle/hspec-expectations-json/compare/v1.0.1.0...main)
 
+## [v1.0.2.0](https://github.com/freckle/hspec-expectations-json/compare/v1.0.1.1...v1.0.2.0)
+
+- Add `shouldBeJsonNormalized` and `Normalizer` to better support configurable matching
+- Added new option for `treatNullsAsMissing` that will treat nulls fields as if they are the same as omitted ones when doing a comparison
+
+## [v1.0.1.1](https://github.com/freckle/hspec-expectations-json/compare/v1.0.1.0...v1.0.1.1)
+
+- Add invariant for all matchers for equality. (ex: forall a. a `shouldMatchJson` a)
+
 ## [v1.0.1.0](https://github.com/freckle/hspec-expectations-json/compare/v1.0.0.6...v1.0.1.0)
 
 - Colorize diff in expectation failure, if connected to a terminal or running on
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,5 +1,10 @@
 # Hspec Expectations for JSON Values
 
+[![Hackage](https://img.shields.io/hackage/v/hspec-expectations-json.svg?style=flat)](https://hackage.haskell.org/package/hspec-expectations-json)
+[![Stackage Nightly](http://stackage.org/package/hspec-expectations-json/badge/nightly)](http://stackage.org/nightly/package/hspec-expectations-json)
+[![Stackage LTS](http://stackage.org/package/hspec-expectations-json/badge/lts)](http://stackage.org/lts/package/hspec-expectations-json)
+[![CI](https://github.com/freckle/hspec-expectations-json/actions/workflows/ci.yml/badge.svg)](https://github.com/freckle/hspec-expectations-json/actions/workflows/ci.yml)
+
 Comparing JSON `Value`s in Haskell tests comes with some challenges:
 
 - In API responses, additive changes are typically safe and an important way to
diff --git a/hspec-expectations-json.cabal b/hspec-expectations-json.cabal
--- a/hspec-expectations-json.cabal
+++ b/hspec-expectations-json.cabal
@@ -1,6 +1,6 @@
 cabal-version:   1.18
 name:            hspec-expectations-json
-version:         1.0.0.7
+version:         1.0.2.0
 license:         MIT
 license-file:    LICENSE
 copyright:       2020 Freckle Education
@@ -59,15 +59,15 @@
         TypeApplications TypeFamilies
 
     build-depends:
-        Diff >=0.3.4,
-        HUnit >=1.6.0.0,
-        aeson >=1.3.1.1,
-        aeson-pretty >=0.8.7,
+        Diff,
+        HUnit,
+        aeson,
+        aeson-pretty,
         base >=4.11 && <5,
-        scientific >=0.3.6.2,
-        text >=1.2.3.1,
-        unordered-containers >=0.2.9.0,
-        vector >=0.12.0.2
+        scientific,
+        text,
+        unordered-containers,
+        vector
 
 test-suite spec
     type:               exitcode-stdio-1.0
@@ -89,7 +89,9 @@
         TypeApplications TypeFamilies
 
     build-depends:
-        aeson-qq >=0.8.2,
+        QuickCheck,
+        aeson,
+        aeson-qq,
         base >=4.11 && <5,
-        hspec >=2.5.5,
-        hspec-expectations-json -any
+        hspec,
+        hspec-expectations-json
diff --git a/library/Test/Hspec/Expectations/Json.hs b/library/Test/Hspec/Expectations/Json.hs
--- a/library/Test/Hspec/Expectations/Json.hs
+++ b/library/Test/Hspec/Expectations/Json.hs
@@ -13,27 +13,49 @@
 -- +--------------------------+-------------------+-------------------+
 -- | 'shouldMatchOrderedJson' | No                | Yes               |
 -- +--------------------------+-------------------+-------------------+
---
 module Test.Hspec.Expectations.Json
-  ( shouldBeJson
+  ( shouldMatchJson
+  , shouldBeJson
+  , shouldBeJsonNormalized
+  , Normalizer
+  , defaultNormalizer
+  , treatNullsAsMissing
+  , ignoreArrayOrdering
+  , subsetActualToExpected
+  , expandHeterogenousArrays
+
+    -- * Legacy API
+
+    -- | Prefer to use shouldBeJsonNormalized with the appropriate 'Normalizer'
   , shouldBeUnorderedJson
-  , shouldMatchJson
   , shouldMatchOrderedJson
 
-  -- * As predicates
-  -- | These are only created when a specific need arises
+    -- * As predicates
+
+    -- | These are only created when a specific need arises
   , matchesJson
-  )
-where
+  ) where
 
 import Prelude
 
+import Control.Monad (unless)
 import Data.Aeson
 import Data.Aeson.Encode.Pretty (encodePretty)
+import Data.Bifunctor
+import Data.Semigroup (Endo (..))
 import Data.Text.Lazy (toStrict)
 import Data.Text.Lazy.Encoding (decodeUtf8)
 import GHC.Stack
 import Test.Hspec.Expectations.Json.Internal
+  ( Subset (..)
+  , Superset (..)
+  , assertBoolWithDiff
+  , filterNullFields
+  , normalizeScientific
+  , pruneJson
+  , sortJsonArrays
+  )
+import qualified Test.Hspec.Expectations.Json.Internal as Internal
 
 -- $setup
 -- >>> :set -XQuasiQuotes
@@ -43,6 +65,48 @@
 -- >>> let printFailure (HUnitFailure _ r) = putStr $ formatFailureReason r
 -- >>> let catchFailure f = handle printFailure $ f >> putStrLn "<passed>"
 
+newtype Actual a = Actual a
+  deriving (Functor)
+
+newtype Expected a = Expected a
+  deriving (Functor)
+
+newtype Normalizer = Normalizer
+  { normalize :: Endo (Actual Value, Expected Value)
+  }
+  deriving newtype (Semigroup, Monoid)
+
+normalizeBoth :: (Value -> Value) -> Normalizer
+normalizeBoth f = Normalizer $ Endo $ bimap (fmap f) (fmap f)
+
+treatNullsAsMissing :: Normalizer
+treatNullsAsMissing = normalizeBoth filterNullFields
+
+ignoreArrayOrdering :: Normalizer
+ignoreArrayOrdering = normalizeBoth sortJsonArrays
+
+expandHeterogenousArrays :: Normalizer
+expandHeterogenousArrays = normalizeBoth Internal.expandHeterogenousArrays
+
+subsetActualToExpected :: Normalizer
+subsetActualToExpected = Normalizer $ Endo go
+ where
+  go (Actual a, Expected b) =
+    let a' = pruneJson (Superset a) (Subset b)
+    in  (Actual a', Expected b)
+
+defaultNormalizer :: Normalizer
+defaultNormalizer =
+  ignoreArrayOrdering <> subsetActualToExpected
+
+shouldBeJsonNormalized :: HasCallStack => Normalizer -> Value -> Value -> IO ()
+shouldBeJsonNormalized normalizer a b =
+  unless (a == b) $
+    assertBoolWithDiff (a' == b') (toText b) (toText a)
+ where
+  toText = toStrict . decodeUtf8 . encodePretty . normalizeScientific
+  (Actual a', Expected b') = appEndo (normalize normalizer) (Actual a, Expected b)
+
 -- | Compare two JSON values, with a useful diff
 --
 -- >>> :{
@@ -62,10 +126,8 @@
 -- ---    "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
+shouldBeJson = shouldBeJsonNormalized mempty
 
 infix 1 `shouldBeJson`
 
@@ -92,9 +154,8 @@
 -- +++    "b": false,
 -- +++    "c": true
 --    }
---
 shouldBeUnorderedJson :: HasCallStack => Value -> Value -> IO ()
-shouldBeUnorderedJson a b = sortJsonArrays a `shouldBeJson` sortJsonArrays b
+shouldBeUnorderedJson = shouldBeJsonNormalized ignoreArrayOrdering
 
 infix 1 `shouldBeUnorderedJson`
 
@@ -120,18 +181,16 @@
 -- ---    "b": true
 -- +++    "b": false
 --    }
---
 shouldMatchJson :: HasCallStack => Value -> Value -> IO ()
-shouldMatchJson sup sub =
-  sortJsonArrays (pruneJson (Superset sup) (Subset sub))
-    `shouldBeJson` sortJsonArrays sub
+shouldMatchJson = shouldBeJsonNormalized defaultNormalizer
 
 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
+matchesJson sup sub = sup == sub || sup' == sub'
+ where
+  (Actual sup', Expected sub') = appEndo (normalize defaultNormalizer) (Actual sup, Expected sub)
 
 -- | 'shouldBeJson', ignoring extra Object keys
 --
@@ -157,9 +216,7 @@
 -- ---    "b": true
 -- +++    "b": false
 --    }
---
 shouldMatchOrderedJson :: HasCallStack => Value -> Value -> IO ()
-shouldMatchOrderedJson sup sub =
-  pruneJson (Superset sup) (Subset sub) `shouldBeJson` sub
+shouldMatchOrderedJson = shouldBeJsonNormalized subsetActualToExpected
 
 infix 1 `shouldMatchOrderedJson`
diff --git a/library/Test/Hspec/Expectations/Json/Color.hs b/library/Test/Hspec/Expectations/Json/Color.hs
--- a/library/Test/Hspec/Expectations/Json/Color.hs
+++ b/library/Test/Hspec/Expectations/Json/Color.hs
@@ -1,11 +1,11 @@
 module Test.Hspec.Expectations.Json.Color
-  ( Color(..)
+  ( Color (..)
   , getColorize
   ) where
 
 import Prelude
 
-import Control.Monad.IO.Class (MonadIO(..))
+import Control.Monad.IO.Class (MonadIO (..))
 import System.Environment (lookupEnv)
 import System.IO (hIsTerminalDevice, stdout)
 
@@ -18,10 +18,12 @@
   shouldColorize <-
     liftIO $ (||) <$> isGitHubActions <*> hIsTerminalDevice stdout
 
-  pure $ if shouldColorize
-    then \c x -> escape Reset <> escape c <> x <> escape Reset
-    else \_ x -> x
-  where isGitHubActions = (== Just "true") <$> lookupEnv "GITHUB_ACTIONS"
+  pure $
+    if shouldColorize
+      then \c x -> escape Reset <> escape c <> x <> escape Reset
+      else \_ x -> x
+ where
+  isGitHubActions = (== Just "true") <$> lookupEnv "GITHUB_ACTIONS"
 
 escape :: Color -> String
 escape = \case
diff --git a/library/Test/Hspec/Expectations/Json/Internal.hs b/library/Test/Hspec/Expectations/Json/Internal.hs
--- a/library/Test/Hspec/Expectations/Json/Internal.hs
+++ b/library/Test/Hspec/Expectations/Json/Internal.hs
@@ -2,28 +2,28 @@
 
 -- | Internal building-blocks for JSON 'Value' expectations
 module Test.Hspec.Expectations.Json.Internal
-  (
-  -- * Pretty diff
+  ( -- * Pretty diff
     assertBoolWithDiff
 
-  -- * Pruning 'Object's
-  , Superset(..)
-  , Subset(..)
+    -- * Pruning 'Object's
+  , Superset (..)
+  , Subset (..)
   , pruneJson
 
-  -- * Sorting 'Array's
-  , Sortable(..)
+    -- * Sorting 'Array's
+  , Sortable (..)
   , sortJsonArrays
   , vectorSortOn
 
-  -- * Dealing with 'Scientific'
+    -- * Dealing with 'Scientific'
   , normalizeScientific
+  , filterNullFields
+  , expandHeterogenousArrays
   )
 where
 
 import Prelude
 
-
 import Data.Aeson
 #if MIN_VERSION_Diff(0,4,0)
 import Data.Algorithm.Diff (PolyDiff(..), getDiff)
@@ -51,9 +51,10 @@
 assertBoolWithDiff :: HasCallStack => Bool -> Text -> Text -> IO ()
 assertBoolWithDiff asserting expected got = do
   colorize <- getColorize
-  flip HUnit.assertBool asserting . unlines . map (addSign colorize) $ getDiff
-    (lines (T.unpack expected))
-    (lines (T.unpack got))
+  flip HUnit.assertBool asserting . unlines . map (addSign colorize) $
+    getDiff
+      (lines (T.unpack expected))
+      (lines (T.unpack got))
  where
   addSign colorize = \case
     Both _ s -> colorize Reset $ "   " ++ s
@@ -67,9 +68,9 @@
 -- | 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
-    $ KeyMap.intersectionWith (\x y -> pruneJson (Superset x) (Subset y)) a b
-
+  (Object a, Object b) ->
+    Object $
+      KeyMap.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:
@@ -95,11 +96,24 @@
   (Array a, Array b) -> Array $ case b V.!? 0 of
     Nothing -> a
     Just y -> (\x -> pruneJson (Superset x) (Subset y)) <$> a
-
   (x, _) -> x
 
+-- | Expand objects in arrays to have null values for omitted fields
+--
+-- ex: [{a:1}, {b:1}] -> [{a:1, b:null}, {a:null, b:1}]
+expandHeterogenousArrays :: Value -> Value
+expandHeterogenousArrays = go KeyMap.empty
+ where
+  collectAllKeys = \case
+    Object km -> Null <$ km
+    _ -> KeyMap.empty
+  go allKeys = \case
+    Object km -> Object $ expandHeterogenousArrays <$> KeyMap.union km allKeys
+    Array vec -> Array $ go (foldMap collectAllKeys vec) <$> vec
+    x -> x
+
 newtype Sortable = Sortable Value
-  deriving newtype Eq
+  deriving newtype (Eq)
 
 instance Ord Sortable where
   Sortable a `compare` Sortable b = case (a, b) of
@@ -114,25 +128,26 @@
    where
     arbitraryRank :: Value -> Int
     arbitraryRank = \case
-      Object{} -> 5
-      Array{} -> 4
-      String{} -> 3
-      Number{} -> 2
-      Bool{} -> 1
+      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 $ sortJsonArrays <$> hm
-  x@String{} -> x
-  x@Number{} -> x
-  x@Bool{} -> x
-  x@Null{} -> x
+  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
+ where
+  sorted = sortOn f $ V.toList v
 
 -- | Normalize all 'Number' values to 'Double' precision
 --
@@ -143,13 +158,27 @@
 --
 -- 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 $ normalizeScientific <$> hm
   Array vs -> Array $ normalizeScientific <$> vs
-  x@String{} -> x
+  x@String {} -> x
   Number sci ->
     Number $ Scientific.fromFloatDigits @Double $ Scientific.toRealFloat sci
-  x@Bool{} -> x
+  x@Bool {} -> x
   x@Null -> x
+
+filterNullFields :: Value -> Value
+filterNullFields = go
+ where
+  go :: Value -> Value
+  go = \case
+    Object km -> Object $ KeyMap.mapMaybe objectFilter km
+    Array vec -> Array $ filterNullFields <$> vec
+    x -> x
+  objectFilter :: Value -> Maybe Value
+  objectFilter = \case
+    Object km -> Just $ Object $ KeyMap.mapMaybe objectFilter km
+    Array vec -> Just $ Array $ filterNullFields <$> vec
+    Null -> Nothing
+    x -> Just x
diff --git a/library/Test/Hspec/Expectations/Json/Lifted.hs b/library/Test/Hspec/Expectations/Json/Lifted.hs
--- a/library/Test/Hspec/Expectations/Json/Lifted.hs
+++ b/library/Test/Hspec/Expectations/Json/Lifted.hs
@@ -1,16 +1,31 @@
 module Test.Hspec.Expectations.Json.Lifted
-  ( shouldBeJson
+  ( shouldMatchJson
+  , shouldBeJson
+  , shouldBeJsonNormalized
+  , E.Normalizer
+  , E.defaultNormalizer
+  , E.treatNullsAsMissing
+  , E.ignoreArrayOrdering
+  , E.subsetActualToExpected
+  , E.expandHeterogenousArrays
+
+    -- * Legacy API
+
+    -- | Prefer to use shouldBeJsonNormalized with the appropriate 'Normalizer'
   , shouldBeUnorderedJson
-  , shouldMatchJson
   , shouldMatchOrderedJson
   ) where
 
 import Prelude
 
-import Control.Monad.IO.Class (MonadIO(..))
+import Control.Monad.IO.Class (MonadIO (..))
 import Data.Aeson
 import GHC.Stack
 import qualified Test.Hspec.Expectations.Json as E
+
+shouldBeJsonNormalized
+  :: (HasCallStack, MonadIO m) => E.Normalizer -> Value -> Value -> m ()
+shouldBeJsonNormalized n x = liftIO . E.shouldBeJsonNormalized n x
 
 shouldBeJson :: (HasCallStack, MonadIO m) => Value -> Value -> m ()
 shouldBeJson x = liftIO . E.shouldBeJson x
diff --git a/tests/Spec.hs b/tests/Spec.hs
--- a/tests/Spec.hs
+++ b/tests/Spec.hs
@@ -1,2 +1,2 @@
-{-# OPTIONS_GHC -fno-warn-missing-export-lists #-}
 {-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+{-# OPTIONS_GHC -fno-warn-missing-export-lists #-}
diff --git a/tests/Test/Hspec/Expectations/Json/InternalSpec.hs b/tests/Test/Hspec/Expectations/Json/InternalSpec.hs
--- a/tests/Test/Hspec/Expectations/Json/InternalSpec.hs
+++ b/tests/Test/Hspec/Expectations/Json/InternalSpec.hs
@@ -24,13 +24,17 @@
 
     it "prunes object keys recursively Superset not present in Subset" $ do
       let
-        sup = Superset [aesonQQ|
+        sup =
+          Superset
+            [aesonQQ|
           { "foo": { "bar": "bar" , "baz": "baz" }
           , "bat": "bat"
           }
         |]
 
-        sub = Subset [aesonQQ|
+        sub =
+          Subset
+            [aesonQQ|
           { "foo": { "bar": "baz" }
           }
         |]
@@ -39,19 +43,24 @@
 
     it "prunes objects within Arrays" $ do
       let
-        sup = Superset [aesonQQ|
+        sup =
+          Superset
+            [aesonQQ|
           [ { "foo": "bar", "quix": "cats" }
           , { "foo": "bats", "quix": "baz" }
           ]
         |]
 
-        sub = Subset [aesonQQ|
+        sub =
+          Subset
+            [aesonQQ|
           [ { "foo": "zap" }
           , { "foo": "zop" }
           ]
         |]
 
-      pruneJson sup sub `shouldBeJson` [aesonQQ|
+      pruneJson sup sub
+        `shouldBeJson` [aesonQQ|
        [ { "foo": "bar" }
        , { "foo": "bats" }
        ]
@@ -59,19 +68,24 @@
 
     it "handles mismatching types" $ do
       let
-        sup = Superset [aesonQQ|
+        sup =
+          Superset
+            [aesonQQ|
           { "foo": { "bar": 1, "baz": "baz" }
           , "bat": { "bat": 1 }
           }
         |]
 
-        sub = Subset [aesonQQ|
+        sub =
+          Subset
+            [aesonQQ|
           { "foo": { "bar": "baz" }
           , "bat": "bat"
           }
         |]
 
-      pruneJson sup sub `shouldBeJson` [aesonQQ|
+      pruneJson sup sub
+        `shouldBeJson` [aesonQQ|
         { "foo": { "bar": 1 }
         , "bat": { "bat": 1 }
         }
@@ -98,3 +112,25 @@
         sorted = [aesonQQ|[{ "x": ["number_basics", "number_facts"] }]|]
 
       sortJsonArrays unsorted `shouldBeJson` sorted
+
+  describe "filterNullFields" $ do
+    it "filters objects" $ do
+      let
+        withNull = [aesonQQ|{ "x": "x", "y": null }|]
+        withoutNull = [aesonQQ|{ "x": "x" }|]
+
+      filterNullFields withNull `shouldBeJson` withoutNull
+
+    it "filters in arrays" $ do
+      let
+        withNull = [aesonQQ|[{ "x": "x", "y": null }]|]
+        withoutNull = [aesonQQ|[{ "x": "x" }]|]
+
+      filterNullFields withNull `shouldBeJson` withoutNull
+
+    it "filters deeply" $ do
+      let
+        withNull = [aesonQQ|{ "x": {"y": [{"z":"z", "a":null}, {"a": "a"}], "z": null }}|]
+        withoutNull = [aesonQQ|{ "x": {"y": [{"z":"z"}, {"a": "a"}]}}|]
+
+      filterNullFields withNull `shouldBeJson` withoutNull
diff --git a/tests/Test/Hspec/Expectations/JsonSpec.hs b/tests/Test/Hspec/Expectations/JsonSpec.hs
--- a/tests/Test/Hspec/Expectations/JsonSpec.hs
+++ b/tests/Test/Hspec/Expectations/JsonSpec.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE CPP #-}
 {-# LANGUAGE QuasiQuotes #-}
 
 module Test.Hspec.Expectations.JsonSpec
@@ -9,10 +10,32 @@
 import Data.Aeson.QQ
 import Test.Hspec
 import Test.Hspec.Expectations.Json
+#if MIN_VERSION_aeson(2,0,3)
+import Test.Hspec.QuickCheck (prop)
+#endif
 
+matchesJsonTest :: SpecWith ()
+#if MIN_VERSION_aeson(2,0,3)
+matchesJsonTest = describe "matchesJson" $ do
+  prop "always matches itself" $ \a -> a `matchesJson` a
+#else
+matchesJsonTest = pure ()
+#endif
+
 spec :: Spec
 spec = do
+  matchesJsonTest
+
   describe "shouldMatchJson" $ do
+    it "matches itself" $ do
+      let
+        a = [aesonQQ|[{id:1}, {id:2, a:"a"}]|]
+        b = [aesonQQ|[{id:1, a:"a"}, {id:2}]|]
+        c = [aesonQQ|[{id:1, a:"a"}, {id:2, a:"a"}]|]
+      a `shouldMatchJson` a
+      b `shouldMatchJson` b
+      c `shouldMatchJson` c
+
     it "passes regardless of array order" $ do
       let
         a = [aesonQQ|[{ "foo": 1 }, { "foo": 0 }]|]
@@ -52,7 +75,8 @@
         sup4 = [aesonQQ|{ "studentId": 4, "x": true }|]
         sub4 = [aesonQQ|{ "studentId": 4 }|]
 
-        a = [aesonQQ|
+        a =
+          [aesonQQ|
           [ { "stats": [#{sup3}] }
           , { "stats": [#{sup3}, #{sup1}] }
           , { "stats": [#{sup4}, #{sup2}] }
@@ -62,7 +86,8 @@
           ]
         |]
 
-        b = [aesonQQ|
+        b =
+          [aesonQQ|
           [ { "stats": [#{sub1}, #{sub3}] }
           , { "stats": [#{sub3}] }
           , { "stats": [#{sub2}, #{sub4}] }
@@ -76,7 +101,8 @@
 
     it "handles cases where sorting differs after pruning" $ do
       let
-        a = [aesonQQ|
+        a =
+          [aesonQQ|
           [ { "shortName": "B"
             , "subSkills":
               [ { "shortName": "1"
@@ -96,7 +122,8 @@
             }
           ]
         |]
-        b = [aesonQQ|
+        b =
+          [aesonQQ|
           [ { "subSkills":
               [ { "uspId": "71839723561ba1a49cf2a789dbe50302" }
               , { "uspId": "4096d2cfebcab73438971ae2304544ee" }
@@ -111,39 +138,60 @@
 
       a `shouldMatchJson` b
 
-    -- it "is an example failure, to checking how they're printed" $ do
-    --   let
-    --     a = [aesonQQ|
-    --       [ { "shortName": "B"
-    --         , "subSkills":
-    --           [ { "shortName": "1"
-    --             , "uspId": "68fa5xxxxxxxx332f7c88884e5c40e"
-    --             }
-    --           ]
-    --         }
-    --       , { "shortName": "A"
-    --         , "subSkills":
-    --              [ { "shortName": "a"
-    --                , "uspId": "71839723561ba1a49cf2a789dbe50302"
-    --                }
-    --              , { "shortName": "b"
-    --                , "uspId": "4096d2cfebcab73438971ae2304544ee"
-    --                }
-    --              ]
-    --         }
-    --       ]
-    --     |]
-    --     b = [aesonQQ|
-    --       [ { "subSkills":
-    --           [ { "uspId": "71839723561ba1a49cf2a789dbe50302" }
-    --           , { "uspId": "4096d2cfebcab73438971ae2304544ee" }
-    --           ]
-    --         }
-    --       , { "subSkills":
-    --           [ { "uspId": "68fa57ddbc1e9aa332f7c88884e5c40e" }
-    --           ]
-    --         }
-    --       ]
-    --     |]
+    let shouldMatchJsonWithOmittedNullFields =
+          shouldBeJsonNormalized $
+            treatNullsAsMissing
+              <> ignoreArrayOrdering
+              <> subsetActualToExpected
+              <> expandHeterogenousArrays
 
-    --   a `shouldMatchJson` b
+    it "ignores omitted null fields" $ do
+      let
+        a = [aesonQQ|{ "foo": 1 }|]
+        b = [aesonQQ|{ "foo": 1, "bar": null }|]
+
+      a `shouldMatchJsonWithOmittedNullFields` b
+
+    it "ignores omitted null fields in arrays" $ do
+      let
+        a = [aesonQQ|[{ "bar": 1 }, { "foo": 1 }]|]
+        b = [aesonQQ|[{ "foo": 1, "bar": null }, { "foo":null, "bar": 1 }]|]
+
+      a `shouldMatchJsonWithOmittedNullFields` b
+
+-- it "is an example failure, to checking how they're printed" $ do
+--   let
+--     a = [aesonQQ|
+--       [ { "shortName": "B"
+--         , "subSkills":
+--           [ { "shortName": "1"
+--             , "uspId": "68fa5xxxxxxxx332f7c88884e5c40e"
+--             }
+--           ]
+--         }
+--       , { "shortName": "A"
+--         , "subSkills":
+--              [ { "shortName": "a"
+--                , "uspId": "71839723561ba1a49cf2a789dbe50302"
+--                }
+--              , { "shortName": "b"
+--                , "uspId": "4096d2cfebcab73438971ae2304544ee"
+--                }
+--              ]
+--         }
+--       ]
+--     |]
+--     b = [aesonQQ|
+--       [ { "subSkills":
+--           [ { "uspId": "71839723561ba1a49cf2a789dbe50302" }
+--           , { "uspId": "4096d2cfebcab73438971ae2304544ee" }
+--           ]
+--         }
+--       , { "subSkills":
+--           [ { "uspId": "68fa57ddbc1e9aa332f7c88884e5c40e" }
+--           ]
+--         }
+--       ]
+--     |]
+
+--   a `shouldMatchJson` b
