Elm: Decode a JSON array with a single element into a string - json

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 :-)

Related

Elm 'Json.Decode.succeed': how is it used in a decode pipeline if it is supposed to always return the same value?

I'm learning Elm and one thing that has puzzled me is 'Json.Decode.succeed'. According to the docs
succeed : a -> Decoder a
Ignore the JSON and produce a certain Elm value.
decodeString (succeed 42) "true" == Ok 42
decodeString (succeed 42) "[1,2,3]" == Ok 42
decodeString (succeed 42) "hello" == Err ...
I understand that (although, as a beginner, I don't yet see its use). But this method is also used in a Decode pipeline, thus:
somethingDecoder : Maybe Wookie -> Decoder Something
somethingDecoder maybeWookie =
Json.Decode.succeed Something
|> required "caterpillar" Caterpillar.decoder
|> required "author" (Author.decoder maybeWookie)
What is going on here? That is, if 'succeed' ignores the JSON that's passed to it, how is it used to read JSON and turn it into Elm values? Any clues appreciated!
Just to start, the intuition for a decoder pipeline is that it acts like a curried function where piping with required and optional applies arguments one-by-one. Expect that everything, both the function, its arguments and the return value are all wrapped in Decoders.
So as an example:
succeed Something
|> required (succeed 42)
|> required (succeed "foo")
is equivalent to
succeed (Something 42 "foo")
and
decodeString (succeed (Something 42 "foo")) whatever
will return Ok (Something 42 "foo") as long as whatever is valid JSON.
When everything succeeds it's just a really convoluted function call. The more interesting aspect of decoders, and the reason we use them in the first place, is in the error path. But since 'succeed' is what's of interest here, we'll ignore that and save a lot of time, text and brain cells. Just know that without considering the error path this will all seem very contrived.
Anyway, let's try to recreate this to see how it works.
Decode.map2
The key to the pipelines, apart form the pipe operator, is the Decode.map2 function. You've probably already used it, or its siblings, if you've tried writing JSON decoders without using pipelines. We can implement our example above using map2 like this:
map2 Something
(succeed 42)
(succeed "foo")
This will work exactly like the example above. But the problem with this, from a user POV, is that if we need to add another argument we also have to change map2 to map3. And also Something isn't wrapped in a decoder, which is boring.
Calling functions wrapped in Decoders
The reason this is useful anyway is because it gives us access to several values at the same time, and the ability to combine them in whatever way we want. We can use this to call a function inside a Decoder with an argument inside a Decoder and have the result also wrapped in a Decoder:
map2 (\f x -> f x)
(succeed String.fromInt)
(succeed 42)
Currying and partial application
Unfortunately this still has the problem of needing to change the map function if we need more arguments. If only there was a way to apply arguments to a function one at a time... like if we had currying and partial application. Since we have a way to call functions wrapped in decoders now, what if we return a partially applied function instead and apply the remaining arguments later?
map2 (\f x -> f x)
(succeed Something)
(succeed 42)
will return a Decoder (string -> Something), so now we just have to rinse and repeat with this and the last argument:
map2 (\f x -> f x)
(map2 (\f x -> f x)
(succeed Something)
(succeed 42))
(succeed "")
Et voila, we have now recreated JSON decode pipelines! Although it might not look like it on the surface.
Ceci n'est pas une pipe
The final trick is to use map2 with the pipe operator. The pipe is essentially defined as \x f -> f x. See how similar this looks to the function we've been using? The only difference is that the arguments are swapped around, so we need to swap the order we pass arguments as well:
map2 (|>)
(succeed "")
(map2 (|>)
(succeed 42)
(succeed Something))
and then we can use the pipe operator again to reach the final form
succeed Something
|> map2 (|>)
(succeed 42)
|> map2 (|>)
(succeed "")
It should now be apparent that required is just an alias for map2 (|>).
And that's all there is to it!

Elm decode json in different format

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
]

Elm decode json object with array

Im totally new in ELM. Im trying to get data from get response.
Now im trying to make decoder for that. I have json like this:
{
data: [
{
"price" = 300.5
},
{
"price" = 1005
}
]
}
All i need is get lowest price and return it. (At least return each price).
Now i stacked on Decode.index.
priceDecoder : Decode.Decoder String
priceDecoder =
Decode.field "data" Decode.list
What should i do next?
There are several problems with the question itself. First, you're not posting the error you get. The error I get is this:
The 2nd argument to `field` is not what I expect:
25| Decode.field "data" Decode.list
^^^^^^^^^^^
This `list` value is a:
Decode.Decoder a -> Decode.Decoder (List a)
But `field` needs the 2nd argument to be:
Decode.Decoder a
Second, the JSON you've posted is not valid, data should be enclosed in quotes and properties and values should be separated by :, not =.
Thirdly, the type of priceDecoder seems wrong since the JSON contains no String data, or you're not specifying that you also want to convert the Float to a String. I will assume the type is just wrong.
So, the error you (or at least I) get says that list is a function Decoder a -> Decoder (List a), while it expects just a Decoder a value. This is because list expects a Decoder a to be passed to it, which it will use to decode each item in the list.
The decoder we'll use is Decode.field "price" Decode.float, which will decode the "price" field of the object as a Float.
I'll also change the type of priceDecoder from Decoder String to Decoder (List Float), since the price is a Float and we're decoding a List of them, not just getting the first or last value or something like that. I assume that's what you want since you say "at least return each price'.
The priceDecoder we get then is:
priceDecoder : Decode.Decoder (List Float)
priceDecoder =
Decode.field "data" (Decode.list (Decode.field "price" Decode.float))

How to encode a list of records to JSON in Reason?

Given a record type and a list of records:
type note = {
text: string,
id: string
};
let notes: list complete_note = [{text: "lol", id: "1"}, {text: "lol2", id: "2"}]
How do I encode this to JSON using bs-json module?
What I tried: I tried to manually create JSON string using string interpolation in bucklescript, but that's definitely not something I want to do :)
notes
|> Array.of_list
|> Array.map (
fun x => {
// What should I do?
}
)
|> Json.Encode.stringArray
|> Js.Json.stringify;
Disclaimer, I'm not a Reason expert, so the code might be non-idiomatic. It may also have errors, as I don't have the BuckleScript installed, so I didn't test it.
So, if you want to represent each note as a JSON object with text and id fields, then you can use the Js.Json.objectArray function to create a JSON document from an array of JS dictionaries. The easiest way to create a dictionary would be to use the Js.Dict.fromList function, that takes a list of pairs.
notes
|> Array.of_list
|> Array.map (fun {id, text} => {
Js.Dict.fromList [("text", Js.Json.string text), ("id", Js.Json.string id)]
})
|> Js.Json.objectArray
|> Js.Json.stringify;

Decode JSON into Elm Maybe

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