Futarimiti
Futarimiti

Reputation: 685

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})

Upvotes: 1

Views: 113

Answers (1)

Jon Purdy
Jon Purdy

Reputation: 55069

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

Upvotes: 4

Related Questions