diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,3 +1,3 @@
 # hw-aeson
 
-[![CircleCI](https://circleci.com/gh/haskell-works/hw-aeson.svg?style=svg)](https://circleci.com/gh/haskell-works/hw-aeson)
+[![Binaries](https://github.com/haskell-works/hw-aeson/actions/workflows/haskell.yml/badge.svg)](https://github.com/haskell-works/hw-aeson/actions/workflows/haskell.yml)
diff --git a/hw-aeson.cabal b/hw-aeson.cabal
--- a/hw-aeson.cabal
+++ b/hw-aeson.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.2
 
 name:                   hw-aeson
-version:                0.1.1.0
+version:                0.1.1.1
 synopsis:               Convenience functions for Aeson
 description:            Convenience functions for Aeson.
 category:               Data, JSON
@@ -9,10 +9,10 @@
 bug-reports:            https://github.com/haskell-works/hw-aeson/issues
 author:                 John Ky
 maintainer:             newhoggy@gmail.com
-copyright:              2018-2020 John Ky
+copyright:              2018-2022 John Ky
 license:                BSD-3-Clause
 license-file:           LICENSE
-tested-with:            GHC == 8.10.1, GHC == 8.8.3, GHC == 8.6.5, GHC == 8.4.4
+tested-with:            GHC == 9.2.2, GHC == 9.0.2, GHC == 8.10.7, GHC == 8.8.4, GHC == 8.6.5
 build-type:             Simple
 extra-source-files:     README.md
 
@@ -22,10 +22,10 @@
 
 common base                       { build-depends: base                       >= 4.11       && < 5      }
 
-common doctest                    { build-depends: doctest                    >= 0.16.2     && < 0.17   }
+common doctest                    { build-depends: doctest                    >= 0.16.2     && < 0.21   }
 common doctest-discover           { build-depends: doctest-discover           >= 0.2        && < 0.3    }
-common aeson                      { build-depends: aeson                      >= 1.4        && < 1.5    }
-common hedgehog                   { build-depends: hedgehog                   >= 0.6        && < 1.1    }
+common aeson                      { build-depends: aeson                      >= 1.4        && < 2.1    }
+common hedgehog                   { build-depends: hedgehog                   >= 0.6        && < 1.3    }
 common hspec                      { build-depends: hspec                      >= 2.4        && < 3      }
 common text                       { build-depends: text                       >= 1.2        && < 1.3    }
 
@@ -67,7 +67,7 @@
                       , hw-aeson
   default-language:     Haskell2010
   type:                 exitcode-stdio-1.0
-  ghc-options:          -threaded
+  ghc-options:          -threaded -rtsopts -with-rtsopts=-N
   main-is:              DoctestDriver.hs
   HS-Source-Dirs:       doctest
   build-tool-depends:   doctest-discover:doctest-discover
diff --git a/src/HaskellWorks/Data/Aeson.hs b/src/HaskellWorks/Data/Aeson.hs
--- a/src/HaskellWorks/Data/Aeson.hs
+++ b/src/HaskellWorks/Data/Aeson.hs
@@ -1,13 +1,32 @@
+{-# LANGUAGE DerivingVia #-}
+
 module HaskellWorks.Data.Aeson
-    ( objectWithoutNulls
+    ( JsonEndo(..)
+    , objectWithoutNulls
     , readJson
+    , objectEndo
+    , (.?=)
+    , (.!=)
     ) where
 
-import Data.Aeson
-import Data.Aeson.Types
-import Data.Text
-import Text.Read
+import Text.Read (readMaybe)
+import Data.Aeson (pairs, object, KeyValue((.=)), ToJSON(toJSON, toEncoding), Series, Value(Null))
+import Data.Aeson.Encoding (Encoding)
+import Data.Aeson.Key (Key)
+import Data.Aeson.Types (Pair, Parser)
+import Data.Monoid (Endo(..))
 
+infixr 7 .?=
+infixr 7 .!=
+
+newtype JsonEndo a = JsonEndo
+  { unJsonEndo :: [a] -> [a]
+  }
+  deriving (Semigroup, Monoid) via (Endo [a])
+
+instance KeyValue a => KeyValue (JsonEndo a) where
+  k .= v = JsonEndo (k .= v:)
+
 objectWithoutNulls :: [Pair] -> Value
 objectWithoutNulls = object . Prelude.filter (not . isNull . snd)
   where
@@ -18,3 +37,26 @@
 readJson t s = case readMaybe s of
   Just a  -> pure a
   Nothing -> fail $ "Could not parse " <> t
+
+-- | Render optional fields as missing in JSON output.
+(.?=) :: (KeyValue p, ToJSON v, Monoid p) => Key -> Maybe v -> p
+(.?=) k mv = case mv of
+  Just v -> k .= v
+  Nothing -> mempty
+
+-- | Same as '.=', but with lower precedence to work well with lens.
+(.!=) :: (KeyValue kv, ToJSON v) => Key -> v -> kv
+(.!=) = (.=)
+
+-- | Same as 'object' except used in combination with '.?=' and '.!=' instead of '.='.
+--
+-- For example:
+--
+-- @
+-- 'toJSON' o = 'objectEndo'
+--   [ \"mandatory\" '.!=' o '^.' the @\"mandatory\"
+--   , \"optional\"  '.?=' o '^.' the @\"optional\"
+--   ]
+-- @
+objectEndo :: [JsonEndo Pair] -> Value
+objectEndo es = object $ unJsonEndo (mconcat es) []
diff --git a/test/HaskellWorks/Data/AesonSpec.hs b/test/HaskellWorks/Data/AesonSpec.hs
--- a/test/HaskellWorks/Data/AesonSpec.hs
+++ b/test/HaskellWorks/Data/AesonSpec.hs
@@ -8,7 +8,7 @@
 import HaskellWorks.Data.Aeson
 import Test.Hspec
 
-{-# ANN module ("HLint: ignore Redundant do"  :: String) #-}
+{- HLINT ignore "Redundant do"        -}
 
 spec :: Spec
 spec = describe "HaskellWorks.Data.Aeson" $ do
