waargonaut
waargonaut copied to clipboard
Help: What is the best way to encode optional fields?
Given a record like this:
data Property = SomeProperty {
name :: Text,
description :: Text
}
| FancyProperty {
name :: Text,
description :: Text
total :: Maybe Int,
average :: Maybe Int,
required :: Maybe Bool
}
deriving (Show)
I then want to write a property encoder that looks something like this:
import qualified Waargonaut.Encode as WE
encodeProperty :: Applicative f => WE.Encoder f Property
encodeProperty = WE.mapLikeObj $ \jss ->
WE.textAt "name" (name jss) .
WE.textAt "description" (description jss) .
WE.textAt "type" (encodePropertyType jss) .
WE.boolAt "required" (required jss) -- this field only exists in one of the Property record's constructors
encodePropertyType :: Property -> Text
encodePropertyType x = case x of
SomeProperty {} -> "some"
FancyProperty {} -> "fancy"
You can use either maybeOrNull or maybe. These can be combined in a few different ways to handle optional fields, as well as optional values. Some examples are here, but they might take a bit of squinting. :)
You can pattern match on the constructors like you have in the encodePropertyType function to be able to decide which of the Property constructors you want to encode. An alternative might be to have something like the following:
data FancyProperty = FancyProperty {
total :: Int
average :: Int
required :: Bool
}
data BaseProperty = BaseProperty {
name :: Text,
description :: Text,
fancyProperties :: Maybe FancyProperty
}
I tend to avoid records in sum types as it can become troublesome.
I've ended up with this using the original record sum type:
encodeProperty :: Applicative f => WE.Encoder f J.Property
encodeProperty = WE.mapLikeObj $ \jss ->
encodeProperty' jss .
WE.textAt "name" (name jss) .
WE.textAt "description" (J.description jss)
encodeProperty' :: Property -> WT.MapLikeObj WT.WS WT.Json -> WT.MapLikeObj WT.WS WT.Json
encodeProperty' jss@(SomeProperty {}) =
WE.textAt "type" "some"
encodeProperty' jss@(FancyProperty {}) =
WE.textAt "type" "fancy" .
maybe id (WE.boolAt "required") (required jss)
You've plenty of options for how you choose to encode this, which is kind of the point of the library. :)
Some alternatives:
encodePropertyA :: Applicative f => E.Encoder f Property
encodePropertyA = E.mapLikeObj $ \case
SomeProperty nm desc ->
baseObj "some" nm desc
FancyProperty nm desc tot avg reqd ->
baseObj "fancy" nm desc
. E.atKey' "total" (E.maybeOrNull E.int) tot
. E.atKey' "average" (E.maybeOrNull E.int) avg
. E.atKey' "required" (E.maybeOrNull E.bool)reqd
where
baseObj t n d =
E.textAt "type" t
. E.textAt "name" n
. E.textAt "desc" d
encodePropertyB :: Monad f => E.Encoder f Property
encodePropertyB = E.encodeA $ \p -> E.extendMapLikeObject baseObj p $
case p of
SomeProperty _ _ -> E.atKey' "type" E.text "some"
FancyProperty _ _ tot avg reqd ->
E.atKey' "type" E.text "fancy"
. E.atKey' "total" (E.maybeOrNull E.int) tot
. E.atKey' "average" (E.maybeOrNull E.int) avg
. E.atKey' "required" (E.maybeOrNull E.bool)reqd
where
baseObj = E.objEncoder $ \p ->
E.onObj "name" (name p) E.text mempty >>=
E.onObj "description" (description p) E.text
Thank you for including the example code. In my case I wanted to have the key entirely absent from the object if the value was Nothing rather than provide a default or null. I am writing a little utility to produce JSON Schema files for the input parameters of a set of MS SQL stored procedures.
Waargonaut should be able to support that as well, although giving it a quick poke it is not as straight-forward as I remember.
I might have to play with that and see what falls out, because having the option of either optional values, or optional key:value pairs is definitely a thing that needs to exist.
Also if you work it out and want to point out the bits that didn't work or could be generalised then I'm all ears. :+1: