Encoders for missing fields - use "null" or leave out?

I have been generating some encoders for calling AWS services recently, and having to deal with Maybe fields.

Sometimes it can be important to distinguish between a field being null versus simply missing. For example an HTTP UPATE might interpret a null value as meaning, set this thing to null, but a missing value as leave this thing alone. A PATCH might be more correct in this scenario, and I am sure none of us design APIs that are so flaky, but out in the wild you see this kind of thing all too often.

As you can see the generated code (at the bottom) is a bit noisy. I can think of ways of adding some helper functions to tidy it up. Something along these lines:

module Encode.ExtraExtra exposing (..)

{-| Any field that is `Nothing` is output as `null`. -}
objectWithNulls : List (Maybe ( String, Value )) -> Value

{-| Any field that is `Nothing` is not output at all. -}
objectWithMissing : List (Maybe ( String, Value )) -> Value

field : (String, Value) -> Maybe (String, Value)

optionalField : (String, Maybe a) -> (a -> Value) -> Maybe (String, Value) 

Or I could use some other type than Maybe, perhaps Field to try and make it more explicit that these are fields of an object and can only be used in that context.

In my experience so far, the AWS APIs have been stable under “null” or missing. But it would still be nice to be able to toggle between them with objectWithNulls and objectWithMissing for other APIs, or if I do run into awkward ones on AWS.

Is there a package that already does this? I thought perhaps minibill/elm-codec had the ability to choose between nulls and missing?

===

        encoder val =
            [ Maybe.map (\fld -> ( "VpcConfig", fld |> Codec.encoder vpcConfigCodec )) val.vpcConfig
            , Maybe.map (\fld -> ( "TracingConfig", fld |> Codec.encoder tracingConfigCodec )) val.tracingConfig
            , Maybe.map (\fld -> ( "Timeout", fld |> Codec.encoder timeoutCodec )) val.timeout
            , Maybe.map (\fld -> ( "Tags", fld |> Codec.encoder tagsCodec )) val.tags
            , Just ( "Runtime", val.runtime |> Codec.encoder runtimeCodec )
            , Just ( "Role", val.role |> Codec.encoder roleArnCodec )
            , Maybe.map (\fld -> ( "Publish", fld |> Codec.encoder booleanCodec )) val.publish
            , Maybe.map (\fld -> ( "MemorySize", fld |> Codec.encoder memorySizeCodec )) val.memorySize
            , Maybe.map (\fld -> ( "Layers", fld |> Codec.encoder layerListCodec )) val.layers
            , Maybe.map (\fld -> ( "KMSKeyArn", fld |> Codec.encoder kmskeyArnCodec )) val.kmskeyArn
            , Just ( "Handler", val.handler |> Codec.encoder handlerCodec )
            , Just ( "FunctionName", val.functionName |> Codec.encoder functionNameCodec )
            , Maybe.map (\fld -> ( "Environment", fld |> Codec.encoder environmentCodec )) val.environment
            , Maybe.map (\fld -> ( "Description", fld |> Codec.encoder descriptionCodec )) val.description
            , Maybe.map
                (\fld -> ( "DeadLetterConfig", fld |> Codec.encoder deadLetterConfigCodec ))
                val.deadLetterConfig
            , Just ( "Code", val.code |> Codec.encoder functionCodeCodec )
            ]
                |> Maybe.Extra.values
                |> Json.Encode.object

could https://package.elm-lang.org/packages/harmboschloo/graphql-to-elm/latest/GraphQL-Optional be what you’re looking for?

Thanks, looks like it will do the trick, didn’t know about that one.

I think I’ll make my own package, combining ideas from my outlined API above, and harmboschloo/graphql-to-elm. Since I don’t really want to import a graphql package in situations where I’m not actually using graphql - and I have a couple of tweaks to make to the API too.

Here is the API I settled on:

https://package.elm-lang.org/packages/the-sett/json-optional/latest/Json-Encode-Optional

An example of what an encoder built with it looks like:

import Json.Encode.Optional as Opt
import Json.Encode as Encode exposing (Value)

encoder val =
    [ ( "name", fld.name) |> Opt.field Encode.string
    , ( "age", flg.age) |> Opt.field Encode.int
    , ( "petName", fld.petName) |> Opt.optionalField Encode.string
    ]
       |> Opt.objectMaySkip

Argument order was deliberately chosen to keep the JSON field name and corresponding Elm record field aligned.

1 Like

I like the idea, but I’m struggling with the naming a bit. I don’t like the repetitive Field suffix and I’d probably propose something like required instead of field and drop the suffixes on the rest, so you’d end up with API like

required
optional
skippable
nullable

It is just me thinking out loud, feel free to ignore this thought and keep up the great work you are doing here :wink:

Thanks for your suggestion, I like it. Shorter names (mostly) so less typing. I’ll make a new version with those names.

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.