Decode nested JSON value in Elm (0.18) - json

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

Related

how to get value from json via variable in typescript?

supposing I have to read some data from some json files(i18n), every json file may look like:
{
"foo": "1",
"bar": "2",
...
}
I don't know how many fields this json have(it can be expanded), but it's fields look like
{
[prop: string]: string
}
besides, all the json files share the same fields.
when I try to read a value from this json via:
//a can be expanded, I'm not sure how many fileds does it have
let a = {
name: "dd",
addr: "ee",
}
//I'm confident a has a field "name"
let b = "name";
console.log(a[b]);
the error message is:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type
how could I fix it?
The error you're encountering is because the keys in a is not just any string (in fact, it can only be "name" or "add"), but b can be a string of any arbitrary value. If you are very sure that b represents a key found in the object a, you can hint TypeScript as such:
let b: keyof typeof a = "name";
Attempting to assign any arbitrary string value to b will lead to an error:
// This will cause an error
let b: key typeof a = "foobar";
See proof-of-concept on TypeScript Playground.

Elm JSON decoding into list of objects: BadBody error

I'm super new to elm and I am currently having problem with parsing a HTTP response, which is in the form of JSON. It looks something like this:
[
{
"title":"Songs, Merry and Sad",
"author":"John Charles McNeill"
},
{
"title":"The Ambassadors",
"author":"Henry James"
}
]
Basically, my web app sends a request to my backend, which responds with a JSON file as such. I have created this type alias:
type alias Book =
{ title : String
, author : String}
In addition, here is my HTTP request:
search : Model -> Cmd Msg
search model =
Http.post
{ url = "http://0.0.0.0:5000/search"
, body = Http.multipartBody [Http.stringPart "keyword" model.keyword]
, expect = Http.expectJson GotSearch searchDecoder
}
And here is my decoder:
searchDecoder : Decoder (List Book)
searchDecoder =
Json.Decode.list bookDecoder
bookDecoder : Decoder Book
bookDecoder =
map2 Book
(field "title" string)
(field "author" string)
Despite clearly stating expectJSON in my HTTP request, I get this BadBody error with the following message:
Problem with the value at json[0]:\n\n {\n \"title\": \"Songs, Merry and Sad\",\n \"author\": \"John Charles McNeill\"\n }\n\nExpecting a STRING
I'm not sure what's going on here. Help would be greatly appreciated since I'm super stuck here.

How do I unpack a JSON value into a tagged union type?

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.

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

Parsing out a portion of a JSON file and seeing the data

I found this F# JSON parsing - How to get property using complex path (consisting of several propery names) (I shortened the file string). I am wondering how to show the actual data from the json file in the FSI window?
let soilTest = JsonValue.Load("c:\\,,,soiltest.json")
let query soilTest=
let node = JObject.Parse soilTest
node.SelectToken "person[0].name" |> string
Here is the beginning of the json file:
{
"person": [
{
"name": "John Doe",
"date": "December 1, 2015",
.....
I looked at the link that Mark Seemann provided to see how to get the data out of the json file and be shown in the console.
However when putting
Console.WriteLine(name)
I get this warning:
warning FS0020: This expression should have type 'unit', but has type 'string'. Use 'ignore' to discard the result of the expression, or 'let' to bind the result to a name.
val query : soilTest:string -> unit
Tried |>ignore at the end of console.writeline and I get val query : soulTest:string -> unit
How do you do a let statement from this point to bind the data to it?
soilTest is a of type JsonValue but you're trying to parse it as string, hence the error. Please adjust the path for your environment, otherwise this works for me:
let soilTest = JsonValue.Load(#"c:\tmp\test.json")
let qry (soilTest:JsonValue) =
let data = JObject.Parse (soilTest.ToString())
data.SelectToken "person[0].name" |> string
qry soilTest
//val it : string = "John Doe"
There might be better ways to work with. Json and xml are the bane of my existence...
Edit: e.g. you can access the properties directly, like this:
let soilTest = JsonValue.Load(#"c:\tmp\test.json")
soilTest.["person"].[0].["name"].AsString()
//val it : string = "John Doe"