jsonhaskellaeson

Set default values for omitted fields with Haskell Aeson


I'm using Aeson to accept user configuration in JSON format, where some fields may be omitted and the default values would be used. According to doc I should write something like this:

import           Data.Aeson
import           GHC.Generics

data Person = Person
  { name :: String
  , age  :: Int
  } deriving (Generic, Show)

instance FromJSON Person where
  omittedField = Just $ Person "Unnamed" (-1)
  parseJSON = genericParseJSON defaultOptions { allowOmittedFields = True }

main :: IO ()
main = do print (eitherDecode @Person "{}")
          print (eitherDecode @Person "{\"name\":\"Bob\"}")
          print (eitherDecode @Person "{\"name\":\"Bob\",\"age\":42}")

Does not really work:

Left "Error in $: parsing Main.Person(Person) failed, key \"name\" not found"
Left "Error in $: parsing Main.Person(Person) failed, key \"age\" not found"
Right (Person {name = "Bob", age = 42})

Solution

  • I believe the omittedField definition in your FromJSON instance applies to fields of type Person in a larger data structure, not the fields of Person itself.

    One solution is to make names and ages into their own types, and define omittedField for those types.

    newtype Name = Name String
      deriving (Generic)
    
    instance FromJSON Name where
      omittedField = Just (Name "Unnamed")
      parseJSON = genericParseJSON defaultOptions
    

    However, instead of using the arbitrary values Name "Unnamed" and Age (-1) to signal the absence of a value, you could just as well wrap the fields of Person in Maybe instead.

    data Person = Person
      { name :: Maybe String
      , age  :: Maybe Int
      } deriving (Generic, Show)

    If you need a partially-defined Person with Maybe-wrapped fields in some parts of the code and a fully-defined Person with ordinary fields elsewhere, you could define them both.

    data PersonMaybe = PersonMaybe
      { name :: Maybe String
      , age  :: Maybe Int
      }
    
    data Person = Person
      { name :: String
      , age  :: Int
      }
    

    Then it’s straightforward to convert PersonMaybe -> Maybe Person if needed. The “higher-kinded data” (HKD) pattern can help save some repetition, but it’s a bit more advanced and usually not worthwhile unless you have many fields and more than just 2 states.

    {-# Language TypeFamilies #-}
    
    type family Field f a where
      Field Identity a =   a
      Field f        a = f a
    
    data PersonOf f = PersonOf
      { name :: Field f String
      , age  :: Field f Int
      }
    
    type PersonMaybe = PersonOf Maybe
    type Person      = PersonOf Identity