Conditional JSON Unmarshalling

Background

I’m building a toy webapp to allow users to create, share and vote on various polls (like strawpoll.me). I’m struggling to decode JSON from my API into a type that can exist in one of two states: Single or Multi.

To represent an individual users vote on a given poll I’ve created the following type:

type Selection
    = Single (Maybe String)
    | Multi (Set.Set String)

Reasoning that:

  • If the poll only permits a single vote
    • The user has either
      • Submitted a Vote (Just selection)
      • Not voted at all (Nothing)
  • If the poll allows multiple votes
    • The user has either voted {'A', 'B', 'C'} (for one or more option)
    • Or not voted at all {}

This custom Selection type lives within each poll (fields omitted for clarity)

type alias Poll =
    { pid : String
    , title : String
    , selection : Selection
    }

JSON

I’ve configured my backend API to return information for the two different types of poll as follows, with the type specified by the multi value

{
    "multi": true,
    "votes": ['A', 'B', 'C'] // Multiple Options
    ...
}

And

{
    "multi": false,
   "votes": ['A'] // Single option
}

Ideally I’d like to take this format, and conditionall unmarshal to either Single or Multi based on the value associated with multi.

My Attempt

Here’s what I have so far, loosely based this Stackoverflow answer:

singleDecoder : Decoder Selection
singleDecoder =
    Decode.succeed Single
        |> required "voted" (nullable string)


multiDecoder : Decoder Selection
multiDecoder =
    --  Unmarshal to a JSON array to an Elm Set.Set?


selectionDecoder : Bool -> Decoder Selection
selectionDecoder multi =
    if multi then
        multiDecoder
    else
        singleDecoder


pollDecoder : Decoder Poll
pollDecoder =
    Decode.succeed Poll
        |> required "pid" string
        |> required "title" string
        |> Json.Decode.Pipeline.custom (field "multi" bool |> Decode.andThen selectionDecoder)

I’d hoped that andThen would be help me to create a new decoder based on the multi field.

But I get the following compilation error:

|> Json.Decode.Pipeline.custom (field "multi" bool |> Decode.andThen selectionDecoder)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The argument is:

    Decoder (Selection -> b)

But (|>) is piping it a function that expects:

    Decoder (Bool -> Poll)

I’ve clearly misundersood the behaviour of the andThen function. Does this mean I have to duplicate my Poll decoder datastructure, to handle Single and Multi in order to correctly use andThen or is there something I’m missing?

Are there any examples/resources people can point to that achieve this conditional unmarhsalling behaviour

1 Like

I cannot reproduce your compilation error: Here is an Ellie were it compiles unchanged: https://ellie-app.com/6VLpbkHxpJ5a1

It does not work yet, though, I might look into it later. A couple of points:

  • JSON does not allow for single-quoted strings like in your examples.
  • Is “votes” an array even for a non-multi poll?
  • (It would be nice to have a full example, including imports, ideally on Ellie; it makes getting to the bottom of errors much faster.)

Here’s an updated version of @eike’s solution. It uses Decode.map to finish extracting the data from the values field.

https://ellie-app.com/6VLxzcmGwm8a1

1 Like

Thanks @eike @Titus_Anderson between the two of you my original question has been solved!

I can easily get my backend to return an empty “votes” array value to represent a vote that hasn’t yet been cast. But I’d prefer to instead omit the field and have this automatically handled by the decoder:

{
   "multi": true,
   "title": "Single",
   "pid": "single"
}

I’ve attempted to wrap the decoders with Decode.maybe but things have started getting messy

https://ellie-app.com/6WFLx8hqxH2a1

In this case, you can use Decode.oneOf and Decode.succeed when a field is missing. I believe Decode.maybe requires the field be present but possibly null.

Here’s an updated Ellie:

https://ellie-app.com/6WGcCQBm6Z2a1

1 Like

Actually, looking at the docs, Decode.maybe would work in this situation, I’m just not sure how best to use it for this situation.

Here’s how you could do the multiDecoder using Decode.maybe:

multiDecoder2 =
    Decode.maybe 
        (field "votes" (Decode.list string)
            |> Decode.map Set.fromList
            |> Decode.map Multi
        )
    |> Decode.map (Maybe.withDefault <| Multi Set.empty)

@Titus_Anderson it may be fun to know that under the hood, Decode.maybe is built on top of oneOf :smile:

In general, if you’re decoding two different ways based on the presence of a field in the JSON then I’d recommend using oneOf over maybe if you don’t actually need a Maybe in your data structure.

@Jackevansevo I love how you gave all the background in your original post! It made it really easy to understand your original problem :star_struck:

You’re trying to conditionally decode some JSON into different Elm structures. There are two broad strategies for doing so:

  1. Conditionally decoding based off the value of a field in the JSON (such as your multi field being true). This approach uses the Json.Decode.andThen function like you saw in the StackOverflow answer
  2. Conditionally decoding based on the structure of your JSON (such as the presence of field(s) or the value of a field being of a certain type). Thhis approach uses the Json.Decode.oneOf function as suggested by @Titus_Anderson

You can also use a combination of both these strategies as shown in @Titus_Anderson’s solution :tada:

You may enjoy this article on 5 common JSON decoding scenarios. While all of them deal with conditional decoding in some way or another, the 4th and 5th examples are most relevant to what you’re trying to do here :slightly_smiling_face:

It doesn’t surprise me that “maybe” is built on “oneOf”; it just makes sense. Also, when I was figuring out how to build decoders, I read the article you referenced probably 9 or 10 times, so thanks for that!

1 Like

Many thanks for the help guys!

I feel like the comments/replies here have greatly improved my understanding of JSON decoders in Elm, I wish I’d seen that thoughtbot blog post beforehand. I’ve found the examples super helpful and wish there were more inline examples in the documentation (something akin to the community powered ClojureDocs).

That aside, having dropped the code into my project and recompiled, everything now works as expected. I’ve recorded a little demo here: https://www.youtube.com/watch?v=A7LVD0riD4k&feature=youtu.be

1 Like

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