Json.Decode into Union Type without an identifier

Hi! Very new to learning Elm and was wondering if someone could help simplify a bit of JSON decoding logic or maybe explain the best practice to do what I’ve done.

Basic problem is I have a JSON endpoint that gives me a list of Tasks. Each task has common fields and either some physical data or some virtual data. I have modelled this as below:

type alias Task = { name : String, location : TaskLocation}


type TaskLocation = PhysicalTask Address | VirtualTask WebAddress


type alias Address = { street : String }


type alias WebAddress = { url : String }

Then to decode, I can use Json.Decode.andThen to read the "type" field in the JSON and parse the relevant data into the matching type.

taskDecoder : Json.Decode.Decoder Task
taskDecoder =
    Json.Decode.map2 Task
        (Json.Decode.field "name" Json.Decode.string)
        taskTypeDecoder

taskTypeDecoder : Json.Decode.Decoder TaskLocation
taskTypeDecoder =
    Json.Decode.field "type" Json.Decode.string
        |> Json.Decode.andThen
            (\taskType ->
                case taskType of
                    "physical" ->
                        Json.Decode.map PhysicalTask <| Json.Decode.field "physical" physicalDecoder

                    "virtual" ->
                        Json.Decode.map VirtualTask <| Json.Decode.field "virtual" virtualDecoder

                    _ ->
                        Json.Decode.fail <| "unknown task type: " ++ taskType
            )

So my question is: Is there some way to do this without using the type field? If so, is it worth refactoring to use it?

It feels like I should be able to use Json.Decode.oneOf but that seems to be used for one field only. I’m pretty new to functional programming so could be wrong about that.

Sorry, don’t really know if I’ve explained my question well here. I’m still lacking the ability/vocab for programming this way.

Converting the taskTypeDecoder to a more imperative style I think its doing this:

taskTypeDecoder data =
    match data.type:
        case "physical":
            return physicalDecoder(data.physical)
        case "virtual":
            return virtualDecoder(data.virtual)
        case _:
            throw error

But I’d rather it did this:

taskTypeDecoder data =
    if data.hasattr("physical") and data.physical is not None:
        return physicalDecoder(data.physical)
    elif data.hasattr("virtual") and data.virtual is not None:
        return virtualDecoder(data.virtual)
    else:
        throw error

Sure:

taskTypeDecoder : Json.Decode.Decoder TaskLocation
taskTypeDecoder =
    Json.Decode.oneOf 
        [ Json.Decode.map PhysicalTask <| Json.Decode.field "physical" physicalDecoder
        , Json.Decode.map VirtualTask <| Json.Decode.field "virtual" virtualDecoder
        ]
2 Likes

Oh, thank you! Works great and should be so simple to add more location types in future if needed.

Note that with Json.Decode.oneOf you get worse runtime error messages when you encounter invalid JSON. Worse as in harder to read and understand. I avoid oneOf as much as I can for this reason.

2 Likes

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