waargonaut icon indicating copy to clipboard operation
waargonaut copied to clipboard

Help: What is the best way to encode optional fields?

Open treffynnon opened this issue 6 years ago • 5 comments
trafficstars

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"

treffynnon avatar Feb 14 '19 04:02 treffynnon

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.

mankyKitty avatar Feb 14 '19 04:02 mankyKitty

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)

treffynnon avatar Mar 13 '19 23:03 treffynnon

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

mankyKitty avatar Mar 15 '19 04:03 mankyKitty

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.

treffynnon avatar Mar 15 '19 06:03 treffynnon

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:

mankyKitty avatar Mar 16 '19 00:03 mankyKitty