DEV Community

loading...

#30daysofelm Day 16: Struggling slightly less with JSON

kristianpedersen profile image Kristian Pedersen ・4 min read

This is day 16 of my 30 day Elm challenge

Thanks to Wolfgang Schuster @wolfadex , for helping me out yesterday!

Also, thanks to Sephi at the Elm Slack for helping me out today!

Table of contents

1. Simplifying Main.elm

I couldn't figure out how to deal with JSON yesterday, so I thought it would be a good idea to step back a bit.

Yesterday, I simply copied my project from day 9, Astronomy data from Python in Elm.

This is a project I want to work more on, but its Main.elm is nearly 100 lines of code, with subscriptions, cmd msgs, and other stuff needed to communicate with an external API.

Nothing crazy, but it's enough to get in my way when learning a new concept.

Here's a very minimal starting point that I think will serve me better today:

module Main exposing (main)

import Html exposing (div, text)


main =
    div [] [ text "hi" ]
Enter fullscreen mode Exit fullscreen mode

The first line gets added automatically by elm-format. The standard model-update-view architecture with Browser.sandbox is great, but I don't need it when learning the very basics of decoding JSON.

2. Hardcoded JSON with one field

Here's a very basic (and silly) example of decoding a JSON object with one field.

module Main exposing (main)

import Dict exposing (Dict)
import Html exposing (div, text)
import Json.Decode exposing (..)


name =
    """ {"name": "Kristian"} """


getName =
    field "name" string


main =
    div []
        [ text
            (case decodeString getName name of
                Ok result ->
                    result

                Err e ->
                    e |> Debug.toString
            )
        ]
Enter fullscreen mode Exit fullscreen mode

The triple quotes are neat - they're usually used for multi-line strings, but by using them here, I don't have to put backslashes before every " character.

There are two parts to decoding a JSON object. First, you describe what Elm should get, and then another function does it.

The main bit of code here is decodeString getName name.

Its full name is JSON.Decode.decodeString, and it's what you want for converting anything to a string, I guess. :)

I think this approach without an external API would be a nicer introduction to JSON decoding, as several people have told me that they also struggled a bit to understand it to begin with.

3. Decoding a person object with fields of different types (name, age)

person : String
person =
""" 
    {
        "person": {
            "name": "Kristian",
            "age": 31
        }
    }
"""
Enter fullscreen mode Exit fullscreen mode

This is more complex, but it's also pretty easy to work with.

First, we describe the fields to Elm before doing anything:

howToGetName : Decoder String
howToGetName =
    field "person" (field "name" string)


howToGetAge : Decoder Int
howToGetAge =
    field "person" (field "age" int)


howToGetPerson : Decoder Person
howToGetPerson =
    map2 Person
        (field "name" string)
        (field "age" int)


type alias Person =
    { name : String
    , age : Int
    }
Enter fullscreen mode Exit fullscreen mode

When I wrote this, I just pasted the map2 line from the JSON guide. It doesn't reference the "person" field, so it doesn't work.

Failure "Expecting an OBJECT with a field named 'name'" <internals>

map2 is not the same as List.map, but the way! Its real name is Json.Decode.map2.

Let's compare their type signatures:

map : (a -> b) -> List a -> List b

map2 : (a -> b -> value) -> Decoder a -> Decoder b -> Decoder value

I guess map2 takes two values a and b and returns a value. Then it returns two decoders and turns them into one, or maybe I'm wrong.

Anyway, here's how they're handled. I went for a pretty generic solution, with a Debug.toString that (I think) will convert anything to a string, even strings.

getInfo : String -> Decoder a -> String
getInfo sourceJSON decoder =
    case decodeString decoder sourceJSON of
        Ok result ->
            result |> Debug.toString

        Err e ->
            e |> Debug.toString


main : Html msg
main =
    div []
        [ text (getInfo person howToGetName)
        , text (getInfo person howToGetAge)
        , text (getInfo person howToGetPerson)
        ]

Enter fullscreen mode Exit fullscreen mode

When I do get the last line working, there's one thing I would like to improve. getInfo converts the entire result to a string, but I might want to deal with the individual fields.

getPersonInfo : String -> Decoder Person -> String
getPersonInfo sourceJSON decoder =
    case decodeString decoder sourceJSON of
        Ok { name, age } ->
            name ++ ", " ++ (age |> String.fromInt)

        Err e ->
            e |> Debug.toString


main : Html msg
main =
    div []
        [ text (getInfo person howToGetName)
        , text (getInfo person howToGetAge)
        , text (getPersonInfo person howToGetPerson)
        ]
Enter fullscreen mode Exit fullscreen mode

Instead of saying Ok result, I'm destructuring result as Ok { name, age }. It does compile, so I think I'm on to something.

4. Summary

That's all for today. I feel like I've taken some good steps towards being able to decode my astronomy JSON, and do some fun things with it.

I'm very happy I decided to start from the simplest possible code, rather than dealing with an external API and all the extra code that forces me to keep track of.

See you tomorrow!

Discussion (1)

pic
Editor guide
Collapse
bukkfrig profile image
bukkfrig • Edited

Its full name is JSON.Decode.decodeString, and it's what you want for converting anything to a string, I guess. :)

No, it's for reading from a String into any Elm type.

Instead of getPersonInfo that has to know something about Decoder and do formatting, instead just decode your JSON into a Person and then have a regular viewPerson.

module Main exposing (main)

import Html exposing (..)
import Json.Decode exposing (..)


type alias Person =
    { name : String, age : Int }


viewPerson : Person -> Html msg
viewPerson { name, age } =
    pre []
        [ text ("Name: " ++ name)
        , br [] []
        , text ("Age: " ++ String.fromInt age)
        ]


personDecoder : Decoder Person
personDecoder =
    map2 Person
        (field "name" string)
        (field "age" int)


main : Html msg
main =
    case
        decodeString personDecoder
            """{ "name" : "Kristian", "age" : 31 }"""
    of
        Ok person ->
            viewPerson person

        Err err ->
            pre []
                [ text "Oops: "
                , br [] []
                , text ("    " ++ Debug.toString err)
                ]
Enter fullscreen mode Exit fullscreen mode

You can use Json.Decode.decodeString like this to test your Decoders. Once you're ready to bring back the API stuff, you probably won't have a need for it, and instead you'll just be passing those decoders directly to Http.expectJson.