Given the following JSON:
[
{
"id": 0,
"name": "Item 1",
"desc": "The first item"
},
{
"id": 1,
"name": "Item 2"
}
]
How do you decode that into the following Model:
type alias Model =
{ id : Int
, name : String
, desc : Maybe String
}
Brian Hicks has a series of posts on JSON decoders, you probably want to specifically look at Adding New Fields to Your JSON Decoder (which handles the scenario where you may or may not receive a field from a JSON object).
To start with, you'll probably want to use the elm-decode-pipeline package. You can then use the optional function to declare that your desc field may not be there. As Brian points out in the article, you can use the maybe decoder from the core Json.Decode package, but it will produce Nothing for any failure, not just being null. There is a nullable decoder, which you could also consider using, if you don't want to use the pipeline module.
Your decoder could look something like this:
modelDecoder : Decoder Model
modelDecoder =
decode Model
|> required "id" int
|> required "name" string
|> optional "desc" (Json.map Just string) Nothing
Here's a live example on Ellie.
So if you're looking for a zero-dependency solution that doesn't require Json.Decode.Pipeline.
import Json.Decode as Decode exposing (Decoder)
modelDecoder : Decoder Model
modelDecoder =
Decode.map3 Model
(Decode.field "id" Decode.int)
(Decode.field "name" Decode.string)
(Decode.maybe (Decode.field "desc" Decode.string))
If you want to do this using the Model constructor as an applicative functor (because you'd need more 8 items).
import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Extra as Decode
modelDecoder : Decoder Model
modelDecoder =
Decode.succeed Model
|> Decode.andMap (Decode.field "id" Decode.int)
|> Decode.andMap (Decode.field "name" Decode.string)
|> Decode.andMap (Decode.maybe (Decode.field "desc" Decode.string))
Both of which can be used with Lists with Decode.list modelDecoder. I wish the applicative functions were in the standard library, but you'll have to reach into all of the *-extra libraries to get these features. Knowing how applicative functors work will help you understand more down the line, so I'd suggest reading about them. The Decode Pipeline solution abstracts this simple concept, but when you run into the need for Result.andMap or any other of the andMaps because there's not a mapN for your module or a DSL, you'll know how to get to your solution.
Because of the applicative nature of decoders, all fields should be able to be processed asynchronously and in parallel with a small performance gain, instead of synchronously like andThen, and this applies to every place that you use andMap over andThen. That said, when debugging switching to andThen can give you a place to give yourself an usable error per field that can be changed to andMap when you know everything works again.
Under the hood, JSON.Decode.Pipeline uses Json.Decode.map2 (which is andMap), so there's no performance difference, but uses a DSL that's negligibly more "friendly".
Brian Hicks' "Adding New Fields to Your JSON Decoder" post helped me develop the following. For a working example, see Ellie
import Html exposing (..)
import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Pipeline as JP
import String
type alias Item =
{ id : Int
, name : String
, desc : Maybe String
}
main =
Decode.decodeString (Decode.list itemDecoder) payload
|> toString
|> String.append "JSON "
|> text
itemDecoder : Decoder Item
itemDecoder =
JP.decode Item
|> JP.required "id" Decode.int
|> JP.required "name" Decode.string
|> JP.optional "desc" (Decode.map Just Decode.string) Nothing
Related
I'm building an application with postgrest (backend) and elm (frontend) and right now I'm stuck writing a decoder.
I don't quite understand how to decode to my specific type instead of the base type decoders like int and string.
When I take a look at how string is implemented (line 73 to 75) it's just a call to Elm.Kernel.Json.decodeString which in turn is in the js base of elm.
My JSON looks something like this:
{ "id" : 1
, "parent_id" : 1
, "body" : "text of my body"
}
OR
{ "id" : 1
, "parent_id" : 1
, "sub_parent_id" : 2
}
Is it possible to decode something like that into a single record type (Step) which contains a custom type with multiple constructors to match the two different fields (sub_parent_id and body)
My decoder looks like this but doesn't compile:
import Api.Parent.Step.Types exposing ( Step, StepContent )
import Json.Decode exposing (..)
import Json.Decode.Pipeline exposing (..)
decoder : Decoder Step
decoder =
succeed Step
|> required "id" int
|> oneOf
[ field "stepContent" stepBodyDecoder
, field "stepContent" subStepDecoder
]
stepBodyDecoder : Decoder StepContent
stepBodyDecoder =
succeed StepContent
|> required "body" string
subStepDecoder : Decoder StepContent
subStepDecoder =
succeed StepContent
|> required "sub_parent_id" decoder
Any my Types:
module Api.Parent.Step.Types exposing ( Step, StepContent )
type StepContent
= StepBody String
| SubStep Step
type alias Step =
{ id : Int
, stepContent : StepContent
}
JSON decode pipelines expects succeed to be passed a function, and StepContent is not a function, but a type. Variant constructors are functions however, and if you look at the compiler error it suggests the right fix (although that's somewhat coincidental since it just suggest based on similar names):
I cannot find a `StepContent` variant:
28| succeed StepContent
^^^^^^^^^^^
These names seem close though:
StepBody
Step
OneOf
SubStep
StepBody and SubStep are the ones you should use instead. stepBodyDecoder will work with just that change, and will get you at least one step further with subStepDecoder, but the type and decoder otherwise don't match the JSON. sub_parent_id is a number, not an object, so it seems like SubStep should take an Int instead of a Step. You can then possibly construct a separate hierarchical data structure in a subsequent step.
The solution is written in the types (as often)
oneOf : List (Decoder a) -> Decoder a
So we have to supply a list of Decoder StepContent, which are the decoder functions.
And as per the relevant comment, to build a StepContent value, we need to form either constructor : a StepBody or a SubStep.
import Api.Parent.Step.Types exposing ( Step, StepContent )
import Json.Decode exposing (..)
import Json.Decode.Pipeline exposing (..)
decoder : Decoder Step
decoder =
succeed Step
|> required "id" int
|> required "stepContent" (oneOf [ stepBodyDecoder, subStepDecoder ] )
stepBodyDecoder : Decoder StepContent
stepBodyDecoder =
succeed StepBody
|> required "body" string
subStepDecoder : Decoder StepContent
subStepDecoder =
succeed SubStep
|> required "sub_parent_id" decoder
I try to decode some json in elm.
The object I´m receiving could be in two different shapes.
First case:
{
...
"ChipId": "NotSet"
...
}
Second Case:
{
...
"ChipId": {
"Item": "0000000000"
},
...
}
So the first one can easily be decoded with field "ChipId" string but in case it´s the complex object it fails.
I already tried it with Decode.andThen but I couldn't solve it.
Thank you for your help!
Update 1 - decoder that fails
The way I tried was by using a Maybe.
chipIdDecoder : Decoder String
chipIdDecoder =
let
chipIdIdDecoder : Decoder String
chipIdIdDecoder =
field "ChipId" (field "Fields" (firstElementDecoder string))
chooseChipId : Maybe String -> Decoder String
chooseChipId c =
case c of
Just id ->
case id of
"ChipId" ->
chipIdIdDecoder
_ ->
succeed ""
Nothing ->
fail "Chip Id is invalid"
in
nullable (field "ChipId" string)
|> andThen chooseChipId
I suppose the problem here is that Maybe expects something or null and not something or something else. ^^
tl;dr: use oneOf
A good approach to writing json decoders in Elm is to start from the smallest parts, write decoders that decode each of those parts independently, then move up to the next level and write a decoder for that by putting together the smaller pieces you've already made.
Here, for example, I would start by writing a decoder to handle the two possible forms of "ChipId" separately. The first is just a string, which of course comes out of the box with elm/json, so that's easy enough. The other is an object with a single field, which we'll just decode into a simple String:
chipIdObjectDecoder : Decoder String
chipIdObjectDecoder =
field "Item" string
Then we need to put them together, which seems like the part you're struggling most with. Here the oneOf function comes to our rescue, whose description says:
Try a bunch of different decoders. This can be useful if the JSON may come in a couple different formats.
Sounds like exactly what we need! To try both the string decoder and our chipIdObjectDecoder we can write:
eitherStringOrChipIdObject : Decoder String
eitherStringOrChipIdObject =
oneOf [ string, chipIdObjectDecoder ]
And then finally we need to decode the "ChipId" field itself:
field "ChipId" eitherStringOrChipIdObject
All this put together in a single function:
chipIdDecoder : Decoder String
chipIdDecoder =
let
chipIdObjectDecoder : Decoder String
chipIdObjectDecoder =
field "Item" string
eitherStringOrChipIdObject : Decoder String
eitherStringOrChipIdObject =
oneOf [ string, chipIdObjectDecoder ]
in
field "ChipId" eitherStringOrChipIdObject
Or to simplify it a little bit, since the above is rather verbose:
chipIdDecoder : Decoder String
chipIdDecoder =
let
chipIdObjectDecoder =
field "Item" string
in
field "ChipId" (oneOf [ string, chipIdObjectDecoder ])
As a last note, since it's not clear whether your code is overly simplified. If the "ChipId" object cannot be reduced to a simple string, you'll have to use a common type that can hold both a String and a ChipIdObject, and map the decoded values to that common type. eitherStringOrChipIdObject could then be something like this:
type alias ChipIdObject = { ... }
type ChipId
= ChipIdString String
| ChipIdObject ChipIdObject
eitherStringOrChipIdObject : Decoder ChipId
eitherStringOrChipIdObject =
oneOf
[ string |> map ChipIdString
, chipIdObjectDecoder |> map ChipIdObject
]
I've got some JSON coming from firebase that looks like this
{
"type": "added",
"doc": {
"id": "asda98j1234jknkj3n",
"data": {
"title": "Foo",
"subtitle": "Baz"
}
}
}
Type can be one of "added", "modified" or "removed". Doc contains an id and a data field. The data field can be any shape and I am able to decode that properly.
I want to use union types to represent these values like so,
type alias Doc data =
(String, data)
type DocChange doc
= Added doc
| Modified doc
| Removed doc
Here the Doc type alias represents the value contained in the doc field in the JSON above. DocChange represents the whole thing. If the type is say "added", then the JSON must decode into Added doc and so on. I don't understand how to decode union types.
I think the andThen function from Json.Decode looks like what I need, but I am unable to use it correctly.
First of all, it seems like you want to constrain the doc parameter of DocChange to a Doc, so you should probably define it like this instead:
type DocChange data
= Added (Doc data)
| Modified (Doc data)
| Removed (Doc data)
Otherwise you'll have to repeatedly specify DocChange (Doc data) in your functions type annotations which quickly gets annoying, and worse the more you nest it. In any case, I've continued using the types as you defined them:
decodeDocData : Decoder DocData
decodeDocData =
map2 DocData
(field "title" string)
(field "subtitle" string)
decodeDoc : Decoder data -> Decoder (Doc data)
decodeDoc dataDecoder =
map2 Tuple.pair
(field "id" string)
(field "data" dataDecoder)
decodeDocChange : Decoder data -> Decoder (DocChange (Doc data))
decodeDocChange dataDecoder =
field "type" string
|> andThen
(\typ ->
case typ of
"added" ->
map Added
(field "doc" (decodeDoc dataDecoder))
"modified" ->
map Modified
(field "doc" (decodeDoc dataDecoder))
"removed" ->
map Removed
(field "doc" (decodeDoc dataDecoder))
_ ->
fail ("Unknown DocChange type: " ++ typ)
)
The trick here is to decode "type" first, then use andThen to switch on it and choose the appropriate decoder. In this case the shape is identical across "types", but it may not be and this pattern gives the flexibility to handle diverging shapes as well. It could be simplified to just selecting the constructor and keeping the rest of the decoding common if you're absolutely sure they won't diverge.
had a look for something like this, but can't find the exact issue.
I have a JSON back from server side validation that looks like:
{
"field": ["field-name"],
"messages":["message","message"]
}
What I would like to do is decode it into an elm record like
{ field: String, messages: List String }
However, I'm having trouble with the err, field field. I'm having trouble turning a single element JSON array into just a string of that element.
Is it even possible with Decode, or am I better of Decoding it into a List and then just grabbing the head from the list.
This is what I have for the decode:
valErrorDecoder : Decode.Decoder ValError
valErrorDecoder =
decode ValError
|> required "field" (Decode.list Decode.string)
|> required "messages" (Decode.list Decode.string)
Thanks for any help!
Try Decode.index, that should do the trick.
valErrorDecoder : Decode.Decoder ValError
valErrorDecoder =
decode ValError
|> required "field" (Decode.index 0 Decode.string)
|> required "messages" (Decode.list Decode.string)
You mention in a comment that a colleague suggested Decode.map. In case you're curious, here is what that (more complex) solution might look like:
firstElementDecoder : Decode.Decoder a -> Decode.Decoder a
firstElementDecoder baseDecoder = Decode.list baseDecoder
|> Decode.map List.head
|> Decode.andThen (Maybe.map Decode.succeed >> Maybe.withDefault (Decode.fail "Empty list"))
What's happening here? We begin by decoding a list of strings, then map the List.head function onto that list, giving a Decoder (Maybe String). The function
Maybe.map Decode.succeed
>> Maybe.withDefault (Decode.fail "Empty list")
takes in a Maybe and turns it into a decoder, which either succeeds (with the value of the maybe) or fails (with an "Empty list" error message). We use this function as the argument to Decode.andThen, which:
Passes the Maybe from the list decoder into the function above, getting either a Decode.succeed or a Decode.fail
Runs the returned decoder, succeeding or failing with the appropriate value.
So, yes, Decode.index 0 is easier! But it may be of interest to see the longer solution, too :-)
Given the following types:
type alias Wrapper =
{ data : Data }
type alias Data =
{ name : String }
And the following JSON:
{"data": {"name": "Keith"}}
How can I write a decoder which will allow me to turn an HTTP response into an instance of the Wrapper type alias?
I've tried a number of approaches using the core libraries, Json.Decode.Pipeline and Json.Decode.Extra, without finding a workable solution.
Here's my latest attempt:
dataDecoder =
succeed Data
|> andMap (field "name" Decode.string)
wrapperDecoder =
succeed Wrapper
|> andMap (field "data" dataDecoder)
Which results in:
BadPayload "Expecting an object with a field named name but instead got: {\"data\":{\"name\":\"foo\"}}" { status = { code = 200, message = "OK" }, headers = Dict.fromList [("cache-control","max-age=0, private, must-revalidate"),("content-type","application/json; charset=utf-8")], url = "http://localhost:5000//users/foo", body = "{\"data\":{\"name\":\"foo\"}}" }
EDIT:
This wound up being an end-user problem. I was passing the correct decoder to Http.post, but Http.send wasn't actually calling the function wrapping Http.post. Doh.
Your decoders work fine against your example input, but the error message you are getting leads me to believe you are trying to use dataDecoder in your Http call rather than wrapperDecoder, since the error message is looking for a field named name.
While succeed and andMap can be used to construct your decoder, you can get by with map:
dataDecoder : Decoder Data
dataDecoder =
Decode.map Data (field "name" string)
wrapperDecoder : Decoder Wrapper
wrapperDecoder =
Decode.map Wrapper (field "data" dataDecoder)
As Chad Gilbert wrote, your decoders are fine: https://ellie-app.com/kDX99XRbta1/0
To doublecheck, add type annotations to your decoders:
dataDecoder : Decoder Data
dataDecoder = ...
wrapperDecoder : Decoder Wrapper
wrapperDecoder = ...
If you're really using wrapperDecoder (Http.post apiUrl body wrapperDecoder), there's one more possibility for an error: that your API endpoint returns data with a different shape, something like:
{"data": {"data": {"name": "foo"}}}
Can you doublecheck this? (in Chrome's Web Inspector etc.)