insight.insight
insight.insight

Reputation: 31

How to decode a heterogenous array with remaining values as a list

I want to decode json string like below.

"[[\"aaa\",1,2,3,4],[\"bbb\",1,2,3]]"

and decode to Elm tuple list.

[("aaa",[1,2,3,4]),("bbb",[1,2,3])] : List (String, List Int)

How to decode it?

jsdecode=index 0 string
    |> andThen xxxxxxx??

Upvotes: 3

Views: 238

Answers (2)

Ju Liu
Ju Liu

Reputation: 3999

This isn't straightforward to do, but before I jump straight in how to do it, let me collect a series of thoughts about the data we are trying to decode:

  • We are decoding a list of lists
  • Each list should be composed by a starting string and a series of values
  • But actually there might be an empty list, no initial string but some values or an initial string and no values

So in my mind the difficulty of building the right decoder reflects the complexity of handling all these edge cases. But let's start defining the data we would like to have:

type alias Record =
    ( String, List Int )


type alias Model =
    List Record

jsonString : String
jsonString =
    "[[\"aaa\",1,2,3,4],[\"bbb\",1,2,3]]"

decoder : Decoder Model
decoder =
    Decode.list recordDecoder

Now we need to define a type that represents that the list could contain either strings or ints

type EntryFlags
    = EntryId String
    | EntryValue Int


type RecordFlags
    = List EntryFlags

And now for our decoder

recordDecoder : Decoder Record
recordDecoder =
    Decode.list
        (Decode.oneOf
            [ Decode.map EntryId Decode.string
            , Decode.map EntryValue Decode.int
            ]
        )
        |> Decode.andThen buildRecord

So buildRecord takes this list of EntryId String or EntryValue Int and builds the record we are looking for.

buildRecord : List EntryFlags -> Decoder Record
buildRecord list =
    case list of
        [] ->
            Decode.fail "No values were passed"

        [ x ] ->
            Decode.fail "Only key passed, but no values"

        x :: xs ->
            case buildRecordFromFlags x xs of
                Nothing ->
                    Decode.fail "Could not build record"

                Just value ->
                    Decode.succeed value

As you can see, we are dealing with a lot of edge cases in our decoder. Now for the last bit let's check out buildRecordFromFlags:

buildRecordFromFlags : EntryFlags -> List EntryFlags -> Maybe Record
buildRecordFromFlags idEntry valueEntries =
    let
        maybeId =
            case idEntry of
                EntryId value ->
                    Just value

                _ ->
                    Nothing

        maybeEntries =
            List.map
                (\valueEntry ->
                    case valueEntry of
                        EntryValue value ->
                            Just value

                        _ ->
                            Nothing
                )
                valueEntries
                |> Maybe.Extra.combine
    in
    case ( maybeId, maybeEntries ) of
        ( Just id, Just entries ) ->
            Just ( id, entries )

        _ ->
            Nothing

In this last bit, we are using a function from maybe-extra to verify that all the values following the initial EntryId are indeed all of the EntryValue type.

You can check out a working example here: https://ellie-app.com/3SwvFPjmKYFa1

Upvotes: 2

glennsl
glennsl

Reputation: 29146

There are two subproblems here: 1. decoding the list, and 2. transforming it to the shape you need. You could do it as @SimonH suggests by decoding to a list of JSON values, post processing it and then (or during the post-processing) decode the inner values. I would instead prefer to decode it fully into a custom type first, and then do the post processing entirely in the realm of Elm types.

So, step 1, decoding:

type JsonListValue
    = String String
    | Int Int

decodeListValue : Decode.Decoder JsonListValue
decodeListValue =
    Decode.oneOf
        [ Decode.string |> Decode.map String
        , Decode.int |> Decode.map Int
        ]


decoder : Decode.Decoder (List (List JsonListValue))
decoder =
    Decode.list (Decode.list decodeListValue)

This is a basic pattern you can use to decode any heterogenous array. Just use oneOf to try a list of decoders in order, and map each decoded value to a common type, typically a custom type with a simple constructor for each type of value.

Then onto step 2, the transformation:

extractInts : List JsonListValue -> List Int
extractInts list =
    list
        |> List.foldr
            (\item acc ->
                case item of
                    Int n ->
                        n :: acc

                    _ ->
                        acc
            )
            []


postProcess : List JsonListValue -> Result String ( String, List Int )
postProcess list =
    case list of
        (String first) :: rest ->
            Ok ( first, extractInts rest )

        _ ->
            Err "first item is not a string"

postProcess will match the first item to a String, run extractInts on the rest, which should all be Ints, then put them together into the tuple you want. If the first item is not a String it will return an error.

extractInts folds over each item and adds it to the list if it is an Int and ignores it otherwise. Note that it does not return an error if an item is not an Int, it just doesn't include it.

Both of these functions could have been written to either fail if the values don't conform to the expectations, like postProcess, or to handle it "gracefully", like extractInts. I chose to do one of each just to illustrate how you might do both.

Then, step 3, is to put it together:

Decode.decodeString decoder json
    |> Result.mapError Decode.errorToString
    |> Result.andThen
        (List.map postProcess >> Result.Extra.combine)

Here Result.mapError is used to get error from decoding to conform to error type we get from postProcess. Result.Extra.combine is a function from elm-community/result-extra which turns a List of Results into a Result of List, which comes in very handy here.

Upvotes: 1

Related Questions