Reputation: 31
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
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:
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
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 Int
s, 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 Result
s into a Result
of List
, which comes in very handy here.
Upvotes: 1