Search code examples
jsondecodeelmunion-types

How do I unpack a JSON value into a tagged union type?


I've got some JSON coming from firebase that looks like this

{
  "type": "added",
  "doc": {
    "id": "asda98j1234jknkj3n",
    "data": {
      "title": "Foo",
      "subtitle": "Baz"
    }
  }
}

Type can be one of "added", "modified" or "removed". Doc contains an id and a data field. The data field can be any shape and I am able to decode that properly.

I want to use union types to represent these values like so,

type alias Doc data =
    (String, data)

type DocChange doc
    = Added doc
    | Modified doc
    | Removed doc

Here the Doc type alias represents the value contained in the doc field in the JSON above. DocChange represents the whole thing. If the type is say "added", then the JSON must decode into Added doc and so on. I don't understand how to decode union types.

I think the andThen function from Json.Decode looks like what I need, but I am unable to use it correctly.


Solution

  • First of all, it seems like you want to constrain the doc parameter of DocChange to a Doc, so you should probably define it like this instead:

    type DocChange data
        = Added (Doc data)
        | Modified (Doc data)
        | Removed (Doc data)
    

    Otherwise you'll have to repeatedly specify DocChange (Doc data) in your functions type annotations which quickly gets annoying, and worse the more you nest it. In any case, I've continued using the types as you defined them:

    decodeDocData : Decoder DocData
    decodeDocData =
        map2 DocData
            (field "title" string)
            (field "subtitle" string)
    
    
    decodeDoc : Decoder data -> Decoder (Doc data)
    decodeDoc dataDecoder =
        map2 Tuple.pair
            (field "id" string)
            (field "data" dataDecoder)
    
    
    decodeDocChange : Decoder data -> Decoder (DocChange (Doc data))
    decodeDocChange dataDecoder =
        field "type" string
            |> andThen
                (\typ ->
                    case typ of
                        "added" ->
                            map Added
                                (field "doc" (decodeDoc dataDecoder))
    
                        "modified" ->
                            map Modified
                                (field "doc" (decodeDoc dataDecoder))
    
                        "removed" ->
                            map Removed
                                (field "doc" (decodeDoc dataDecoder))
    
                        _ ->
                            fail ("Unknown DocChange type: " ++ typ)
                )
    

    The trick here is to decode "type" first, then use andThen to switch on it and choose the appropriate decoder. In this case the shape is identical across "types", but it may not be and this pattern gives the flexibility to handle diverging shapes as well. It could be simplified to just selecting the constructor and keeping the rest of the decoding common if you're absolutely sure they won't diverge.