JSON decode unknown number of fields

I want to decode a JSON object that looks like this:

{ 
  "boxes" : {
    "1" : { 
      "items" : {
        "1" : "pencil",
        "2" : "ball"
      },
    },
    "2" : { 
      "items" : {
        "1" : "teapot",
        "2" : "spoon",
        "3" : "towel"
      }
    }
  }
}

I have no control over the JSON format. I get it from an external server.
My problem while decoding is that I don’t know how many boxes the JSON contains nor how many items each box contains.
I would like to decode it into a List of boxes that contains a List of items. How should I approach this?

Thanks in advance!

**Edit: ** I don’t need the numbers of the boxes and the items.

There are a couple different ways to model this data structure. When the keys of an object are dynamic, often you’ll want to model this in Elm with a Dict. However, it looks like they’re really just incrementing with each value so many these are better handled as a List. That’s up to you!

If you go down the Dict path, perhaps your elm model might look like { boxes : Dict String (Dict String String) } (You could also decide to convert the string keys to integers if you wanted). To go down this route, you would use the dict decoder. The dict decoder takes in a decoder for the value and then decodes the keys as strings. The inner items decoder would look something like this: Json.Decode.dict Json.Decode.string and would have the signature: Decoder (Dict String String). You would nest this decoder inside another Dict decoder to make your final decoder.

If you go down the List path, your model could look something like { boxes : List (List String) }. To Decode that, you have to do a couple steps. You could use the key-value pairs decoder to decode the object into a list of the keys and values. You could take that result and transform it into just your items by only taking the values. That decoder works like the dict decoder in that it takes in a decoder for the values and keeps the keys a String. You can decode the items part like this: Json.Decode.keyValuePairs (Json.Decode.String) |> Json.Decode.map (List.map (\(key, value) -> value)). That would decode your object of items into a list of the contained items. This would have the signature Decoder (List String).

I hope that answers your question and points you in the right direction!

Thank you, stephenreddek, for your clear answer. I try to make it work using Json.Decode.keyValuePairs, but that raises a question: are you sure about the pipeline with Json.Decode.map ((key, value) -> value)? Elm doesn’t accept it:

The argument is:

    Decoder (List ( String, String ))

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

    Decoder ( a, value )

I could keep the list of key-value pairs and map them into values only later using List.map. What do you suggest?

Oh I’m sorry! You’re absolutely right. I missed the List.map inside the Decoder.map. I’ll update that in my answer. It’ll just be Json.Decode.keyValuePairs (Json.Decode.String) |> Json.Decode.map (List.map (\(key, value) -> value))

1 Like

The other solution works (I think) but it does simply ignore the information encoded in the numbers and assumes that the fields are always decoded in the right order. This will probably be the case but I think strictly speaking the fields in an object are unordered (so the sender or JavaScript or Elm would be free to present them in a different order). In particular, you should check that keys from “10” on are in the right order because they might also come in lexicographic order where “10” is smaller than “2”.

If that is a problem, I would do something like this and use it to write the “boxes” and “items” decoders

decodeStrangeList : Decoder a -> Decoder (List a)
decodeStrangeList sub =
    Decode.dict sub
    |> Decode.andThen
        (\stringDict ->
            stringDict
            |> Dict.foldl
                (\string value mIntDict ->
                    case (String.toInt string, mIntDict) of
                        (Just int, Just intDict) ->
                            Just (Dict.insert int value intDict)
                        _ ->
                            Nothing
                )
                Just Dict.empty
            |> Maybe.map (Dict.values >> Decode.succeed)
            |> Maybe.withDefault (Decode.fail "One of the keys was not an integer")
        )

(I must admit that I did not test this.) This solution will return the elements in the right order. It does not check that every key exists, so if the indices are important, an even more complicated solution would be necessary.

Thank you very much. It works like a charm!
Still learning a lot. :smiley:

Thanks for this solution too. For now I don’t need the order, but I will keep this in mind for future development.

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