packages feed

hspec-expectations-json 1.0.0.7 → 1.0.2.0

raw patch · 10 files changed

+323/−120 lines, 10 filesdep +QuickCheckdep ~Diffdep ~HUnitdep ~aesonPVP ok

version bump matches the API change (PVP)

Dependencies added: QuickCheck

Dependency ranges changed: Diff, HUnit, aeson, aeson-pretty, aeson-qq, hspec, scientific, text, unordered-containers, vector

API changes (from Hackage documentation)

+ Test.Hspec.Expectations.Json: data Normalizer
+ Test.Hspec.Expectations.Json: defaultNormalizer :: Normalizer
+ Test.Hspec.Expectations.Json: expandHeterogenousArrays :: Normalizer
+ Test.Hspec.Expectations.Json: ignoreArrayOrdering :: Normalizer
+ Test.Hspec.Expectations.Json: instance GHC.Base.Functor Test.Hspec.Expectations.Json.Actual
+ Test.Hspec.Expectations.Json: instance GHC.Base.Functor Test.Hspec.Expectations.Json.Expected
+ Test.Hspec.Expectations.Json: instance GHC.Base.Monoid Test.Hspec.Expectations.Json.Normalizer
+ Test.Hspec.Expectations.Json: instance GHC.Base.Semigroup Test.Hspec.Expectations.Json.Normalizer
+ Test.Hspec.Expectations.Json: shouldBeJsonNormalized :: HasCallStack => Normalizer -> Value -> Value -> IO ()
+ Test.Hspec.Expectations.Json: subsetActualToExpected :: Normalizer
+ Test.Hspec.Expectations.Json: treatNullsAsMissing :: Normalizer
+ Test.Hspec.Expectations.Json.Internal: expandHeterogenousArrays :: Value -> Value
+ Test.Hspec.Expectations.Json.Internal: filterNullFields :: Value -> Value
+ Test.Hspec.Expectations.Json.Lifted: data Normalizer
+ Test.Hspec.Expectations.Json.Lifted: defaultNormalizer :: Normalizer
+ Test.Hspec.Expectations.Json.Lifted: expandHeterogenousArrays :: Normalizer
+ Test.Hspec.Expectations.Json.Lifted: ignoreArrayOrdering :: Normalizer
+ Test.Hspec.Expectations.Json.Lifted: shouldBeJsonNormalized :: (HasCallStack, MonadIO m) => Normalizer -> Value -> Value -> m ()
+ Test.Hspec.Expectations.Json.Lifted: subsetActualToExpected :: Normalizer
+ Test.Hspec.Expectations.Json.Lifted: treatNullsAsMissing :: Normalizer

Files

CHANGELOG.md view
@@ -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
README.md view
@@ -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
hspec-expectations-json.cabal view
@@ -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
library/Test/Hspec/Expectations/Json.hs view
@@ -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`
library/Test/Hspec/Expectations/Json/Color.hs view
@@ -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
library/Test/Hspec/Expectations/Json/Internal.hs view
@@ -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
library/Test/Hspec/Expectations/Json/Lifted.hs view
@@ -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
tests/Spec.hs view
@@ -1,2 +1,2 @@-{-# OPTIONS_GHC -fno-warn-missing-export-lists #-} {-# OPTIONS_GHC -F -pgmF hspec-discover #-}+{-# OPTIONS_GHC -fno-warn-missing-export-lists #-}
tests/Test/Hspec/Expectations/Json/InternalSpec.hs view
@@ -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
tests/Test/Hspec/Expectations/JsonSpec.hs view
@@ -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