Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
treffynnon opened this issue Feb 14, 2019 · 5 comments
Open

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

treffynnon opened this issue Feb 14, 2019 · 5 comments
Labels
how-to Question about using the library or how something works

Comments

@treffynnon
Copy link
Contributor

treffynnon commented Feb 14, 2019

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"
@mankyKitty
Copy link
Contributor

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 mankyKitty added the how-to Question about using the library or how something works label Feb 14, 2019
@treffynnon
Copy link
Contributor Author

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)

@mankyKitty
Copy link
Contributor

mankyKitty commented Mar 15, 2019

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

@treffynnon
Copy link
Contributor Author

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.

@mankyKitty
Copy link
Contributor

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. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
how-to Question about using the library or how something works
Projects
None yet
Development

No branches or pull requests

2 participants