Skip to content

Conversation

@m-bock
Copy link
Contributor

@m-bock m-bock commented Apr 16, 2025

Current state of optional field parsing with optional

Let’s say we have a record with an optional flag field:

type Sample = { flag  Maybe Boolean }

The appropiate codec for this would look like:

codecSample  JsonCodec Sample
codecSample =
  CAR.object "Sample"
    { flag: CAR.optional CA.boolean
    }

This setup is convenient, as it supports multiple JSON inputs:

  • {} is parsed as { flag: Nothing }
  • { "flag": true } is parsed as { flag: Just true }
  • { "flag": false } is parsed as { flag: Just false }

Introducing optionalWith

However, in many use cases, working with a Maybe Boolean isn’t ideal. Often, we’d prefer to use a plain Boolean, treating both Nothing and Just false as false:

type Sample2 = { flag  Boolean }

With the current version of codec-argonaut, we’d need to define both Sample and Sample2, along with conversion functions between them.

To avoid this, the PR introduces a new modifier: optionalWith, a more flexible version of optional.

optionalWith   a b. (Maybe a  b)  (b  Maybe a)  CA.JsonCodec a  OptionalWith a b

Here’s how it can be used:

codecSample2  JsonCodec Sample2
codecSample2 =
  CAR.object "Sample2"
    { flag: CAR.optionalWith
        (fromMaybe false)
        (if _ then Just true else Nothing)
        CA.boolean
    }

This codec handles the following cases directly:

  • {} is decoded as { flag: false }
  • { "flag": true } is decoded as { flag: true }
  • { "flag": false } is decoded as { flag: false }

Other Usecases

This pattern is especially useful when working with JavaScript APIs, where fields are often omitted instead of explicitly set to a default or “empty” value.

Other examples where optionalWith is useful include:

  • Omitting empty foreign objects or arrays
  • Flattening Maybe (Maybe a) to just Maybe a, useful for fields that can be either null or missing

Bonus:
optionalWith is a generalization of optional, which can now internally be redefined as:

optional = optionalWith identity identity

As a result, this MR introduces no code duplication. Tests are added that verify the behavior.

@garyb
Copy link
Owner

garyb commented Apr 20, 2025

I'm not necessarily saying no to this contribution since the ergonomics of this are probably better for simple cases, but are you aware of the use of dimap for codecs too? That's the way I've handled cases like this until now.

@m-bock
Copy link
Contributor Author

m-bock commented Apr 21, 2025

Got it, I guess you mean something like this:

import Prelude

import Data.Codec.Argonaut (JsonCodec)
import Data.Codec.Argonaut as CA
import Data.Codec.Argonaut.Record as CR
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Profunctor (dimap)

type User =
  { name  String
  , age  Int
  , hobbies  Array String
  }

codecUser  JsonCodec User
codecUser =
  dimap
    ( \r → r
        { name = Just r.name
        , age = Just r.age
        , hobbies = if r.hobbies == [] then Nothing else Just r.hobbies
        }
    )
    ( \r → r
        { name = fromMaybe "Max" r.name
        , age = fromMaybe 0 r.age
        , hobbies = fromMaybe [] r.hobbies
        }
    )
    $ CA.object "User"
    $ CR.record
        { name: CR.optional CA.string
        , age: CR.optional CA.int
        , hobbies: CR.optional (CA.array CA.string)
        }

I will close this PR because in the meanwhile I worked on an alternative approach.
I'll probably open another PR and we can discuss further over there.
I think dimap works well but it's also a lot of boilerplate to write. I am looking for something more declarative.

@m-bock m-bock closed this Apr 21, 2025
@garyb
Copy link
Owner

garyb commented Apr 21, 2025

Yeah, exactly, although I've only generally had one field that needed that kind of treatment at a time, so it's a little less boilerplatey - but still not as ergonomic as the field level combinator you're proposing. Just thought I'd mention dimap as it's a useful tool to have!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants