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