Skip to content

Instantly share code, notes, and snippets.

@halan
Forked from yang-wei/decode.md
Created March 12, 2017 03:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save halan/84228b31e52cbd2735bef1c8ddb02de6 to your computer and use it in GitHub Desktop.
Save halan/84228b31e52cbd2735bef1c8ddb02de6 to your computer and use it in GitHub Desktop.
Elm Json.Decode tutorial and cheatsheet

When receiving JSON data from other resources(server API etc), we need Json.Decode to convert the JSON values into Elm values. This gist let you quickly learn how to do that.

I like to follow working example code so this is how the boilerplate will look like:

import Graphics.Element exposing (Element, show)
import Task exposing (Task, andThen)
import Json.Decode exposing (Decoder, int, string, object3, (:=))

import Http

{- declare data type here -}
type alias SimpleRecord =
  { name : String
  , description : String
  }

-- initialize mailbox with the type declared above
mailbox =
  Signal.mailbox (SimpleRecord "" "")

-- VIEW

main : Signal Element
main =
  Signal.map show mailbox.signal

-- TASK

fetchApi =
  Http.get decoder api 

handleResponse data =
  Signal.send mailbox.address data

-- decoder changes depends on our data type
decoder = ...

port run : Task Http.Error ()
port run =
  fetchApi `andThen` handleResponse

api =
  "http://some-api-url.com"

So here we have a mailbox - mailbox which contains initial data depending on the data type. When the application starts, fetch is called and the response is handled by handleResponse. Our main function will display the value of mailbox value. Here, we will fetch data github api and display it on screen. So to follow along this tutorial, you have to modify

  1. data type (currently SimpleRecord)
  2. initial data in mailbox
  3. decoder function
  4. api (to get different data)

Notice that I am omitting some data annotations for brevity. Our main focus in this post will be decoder function. We will see how to utilize the Json.Decoder library when dealing with different type of data.

Objects

Object -> tuple

First of all let's fetch the elm-lang/core repository. The API looks like:

https://api.github.com/repos/elm-lang/core

If you paste this on browser, you will see something like:

{
    "id": 25231002,
    "name": "core",
    ...
    "subscribers_count": 47
}

Let's say we are interested in 3 fields - name, description and watchers_count of this repository and we want to display it in simple tuple: ie (name, description, watchers_count).

Here is how our data type looks like:

type alias RepoTuple = ( String, String, Int )

mailbox =
  Signal.mailbox ("", "", 0)

We also need to declare the initial data in mailbox - ("", "", 0).

Because our API returns a JSON object, the Object fields in doc will give us what we need. In our case we need 3 fields, so we will use object3. Elm supports object1 to object8 and we have to decide depends on how many value we need.

object3
    :  (a -> b -> c -> value)
    -> Decoder a
    -> Decoder b
    -> Decoder c
    -> Decoder value

The first argument (a -> b -> c -> value) is a function which takes 3 arguments and return a value. In Elm, we know that

(,,) "Name" "Description" 100
-- ("Name","Description",100) : ( String, String, number )

So our decoder would be:

repoTupleDecoder : Decoder RepoTuple
repoTupleDecoder =
    object3 (,,)
        ("name" := string)
        ("description" := string)
        ("watchers_count" := int)

The := operator is use to extract the field with the given name. Since we are interested in name, description and watchers_count so we declare it explicitly and state it's type.

fetchApi =
  Http.get repoTupleDecoder api 

And this is the working code for this example.

Object -> record

Often tuple doesn't give enough information, in most case we want to preserve the value of field itself. So our type will look like:

type alias RepoRecord =
  { name : String
  , description : String
  , watchers_count : Int
  }

mailbox =
  Signal.mailbox (RepoRecord "" "" 0)

Note that in this case our data type is Record so we can initialize it by RepoRecord "" "" 0 which will return { name = "", description = "", watchers_count = 0 } which is cool. What cooler is we can even reuse it in our decoder:

repoRecordDecoder : Decoder RepoRecord
repoRecordDecoder =
    object3 RepoRecord
        ("name" := string)
        ("description" := string)
        ("watchers_count" := int)

Do remember to change the function name in fetchApi:

fetchApi =
  Http.get repoRecordDecoder api 

Now we will get a nice record:

{ name = "core", description = "Elm's core libraries", watchers_count = 338 }

Again, for your reference the source is here.

Nested Object

When we hit the API, it returns an object which contains the owner of repository in a nested object.

{
    ...
    "full_name": "elm-lang/core",
    "owner": {
        "login": "elm-lang",
        "id": 4359353,
        "avatar_url": "https://avatars.githubusercontent.com/u/4359353?v=3"
        ...
    },
    ...
}

Let's look at how we can retrive the value in owner object. We can of course create nested decoder by using 2 times of object3 but we can avoid this by using [at](http://package.elm-lang.org/packages/elm-lang/core/3.0.0/Json-Decode#at)

at : List String -> Decoder a -> Decoder a

Changing everything we need:

type alias OwnerRecord =
  { login : String
  , id : Int
  , avatar_url : String
  }

mailbox =
  Signal.mailbox (OwnerRecord "" -1 "")

Because the data we need have only single layer of nested field, so the first argument passed to at only has one value, that is ["owner"].

ownerDecoder : Decoder OwnerRecord
ownerDecoder =
  let
    decoder = object3 OwnerRecord
                ("login" := string)
                ("id" := int)
                ("avatar_url" := string)
  in
    at ["owner"] decoder

Any object -> List (Tuple)

Let's hit another API.

https://api.github.com/repos/elm-lang/elm-lang.org/languages

and it's result is:

{
    "Elm": 400423,
    "JavaScript": 352902,
    "CSS": 75013,
    "Haskell": 28719,
    "HTML": 965
}

In this case, we want all values.

type alias Languages =
  List (String, Int)

mailbox =
  Signal.mailbox []

Elm provides a very handy function - keyValuePairs

import import Json.Decode exposing (..., keyValuePairs)

;; ...

decoder : Decoder (List (String, Int))
decoder =
  keyValuePairs int

And we have all languages in tuple:

[ ("HTML", 965)
, ("Haskell", 28719)
, ("CSS", 75013)
, ("JavaScript", 352902)
, ("Elm", 400423)
]

Any object -> Dict

Besides keyValuePairs, we also has [Decoder.dict](http://package.elm-lang.org/packages/elm-lang/core/3.0.0/Json-Decode#dict) to turn object into dictionary.

type alias Languages =
  Dict String Int

mailbox =
  Signal.mailbox Dict.empty

decoder : Decoder (Dict String Int)
decoder =
  dict int

Arrays

Array -> tuple

In the previous section, we had seen multiples way to deal with object typed JSON value. However sometimes, we have an array. For example:

https://api.github.com/search/repositories?q=language:elm&sort=starts&language=elm

This API returns repositories written in Elm.

"total_count": 1816,
"incomplete_results": false,
"items": [
    {
        "id": 4475362,
        "name": "elm-lang.org",
        "full_name": "elm-lang/elm-lang.org",
        ...
    },
    {
        "id": 25231002,
        "name": "core",
        "full_name": "elm-lang/core",
        ...
    }
]

The field we are particularly interested in is the items field. Let's say we want a list which contains the full_name of the Elm repository.

["elm-lang/elm-lang.org", "elm-lang/core" ...]

Let's initialize our data first:

mailbox =
  Signal.mailbox []

First of all, here is our decoder to extract the full_name value

fullNameDecoder : Decoder String
fullNameDecoder =
  object1 identity ("full_name" := string)

Because our data is nested in the items field, we have to access it using the at operator (hope you still remember):

decoder =
  at ["items"] _

The items will give us an array which contains object, so we will use [Decoder.list](http://package.elm-lang.org/packages/elm-lang/core/3.0.0/Json-Decode#list):

list : Decoder a -> Decoder (List a)

Decoder.list takes a decoder and returns another decoder which can handle list. This suits our case:

decoder =
  at ["items"] (list fullNameDecoder)

Now if you wish to also extract other fields, you just have to change your fullNameDecoder. The source of this example is shown here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment