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
- data type (currently SimpleRecord)
- initial data in
mailbox
- decoder function
- 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.
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.
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.
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
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)
]
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
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.
Thanks for writing this!