How to decode json based on previous value?

I am using elm/json to decode results from a network call. I have this situation where I get JSON that looks like this:

{ "action_type": "InsertNewExercise",
  "payload": {
        "day": 1,
        "order": 3,
        "exercise": {
            "name": "Deadlift",
            "sets": [
                {
                    "reps": 8,
                    "weight": 185
                },
                {
                    "reps": 8,
                    "weight": 185
                }
            ]
        }
    }
}

The payload and action_type are linked, so I want to create a decoder which attempts to decode payload given some value of action_type:

decodePayload : String -> D.Decoder Action
decodePayload action =
    case action of
        "InsertNewExercise" ->
            D.map Insert decodeInsert

        _ ->
            D.fail ("Failed to decode this action: " ++ action)

I used the JSON.andThen documentation to write this decoder:

payload: D.Decoder Action
payload = D.field "action_type" D.string
    |> D.andThen decodePayload

But to actually use, I don’t have a way to not nest the JSON:

decode = D.map RowResult (D.field "payload" payload)

I obviously get the error that field action_type was not found inside the payload.

How do I get it to not have to nest?

EDIT: I made it work with a real ugly approach. Essentially I created an intermediate representation of the raw value and then worked over it:

-- types: notice the new "raw" type --
type Action = Action1 ActionBody | Action2 ActionBody
type alias Row = { action: Action }
type alias RawRow = { actionType: String, actionBody: D.Value }


rawDecoder: D.Decoder RawRow
rawDecoder = D.map2 RawRow
  ( D.field "action_type" D.string )
  ( D.field "payload" D.value )


rowWithAction: D.Decoder Action -> D.Decoder Row
rowWithAction decoder = D.field "payload" decoder

 -- implementation as above --
decodePayload : String -> D.Decoder Action

-- this decoder works -- 
decoder: D.Decoder Row
decoder = D.andThen (\raw -> ( rowWithAction <| decodePayload row.actionType ) ) rawDecoder

This does work, but it’s ugly. Would love to hear of a more elegant solution!

All that is missing is to target the field payload.

decoder : D.Decoder Insert
decoder =
    D.field "action_type" D.string
        |> D.andThen decodePayload


decodePayload action =
    case action of
        "InsertNewExercise" ->
            D.field "payload" decodeInsert

        _ ->
            D.fail ("Failed to decode this action: " ++ action)


type alias Insert =
    { day : Int, order : Int }


decodeInsert =
    D.map2 Insert
        (D.field "day" D.int)
        (D.field "order" D.int)

So
D.field "payload" decodeInsert instead of D.map Insert decodeInsert

I made myself this little helper:

switch : String -> List ( String, Decoder a ) -> Decoder String -> Decoder a
switch notFoundMsg groups keyDecoder =
    let
        getDecoder key =
            Dict.get key (groups |> Dict.fromList)
                |> Maybe.withDefault (Decode.fail <| notFoundMsg ++ ": " ++ key)
    in
    Decode.andThen getDecoder keyDecoder

Then I would use it like this:

type Action = InsertNewExerciseAction NewExerciseData | SomeOtherAction

type alias NewExerciseData = {
-- Your data here
}

actionDecoder: String -> Decoder Action
actionDecoder action =
    Decode.field "action_type" Decode.string
        |> switch "this action type does not exist"
            [ ( "InsertNewExercise", Decode.field "value" insertNewExerciseDecoder |> Decode.map InsertNewExerciseAction )
            ]

This is an “idiomatic” way of writing that:

actionDecoder : String -> Decoder Action
actionDecoder action =
    Decode.field "action_type" Decode.string
        |> Decode.andThen
            (\actionType ->
                case actionType of
                    "InsertNewExercise" ->
                        Decode.field "value" insertNewExerciseDecoder
                            |> Decode.map InsertNewExerciseAction

                    _ ->
                        Decode.fail ("this action type does not exist: " ++ actionType)
            )

You’ll almost certainly encounter something like that if you work on somebody else’s code base at least.

1 Like

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