Json and
Elm decoders

GET / HTTP/1.1
<html>
 <head>...</head>
 <body>
   <h1>Wonderfull site!</h1>
   <p>Hello</p>
 </body>
</html>
GET / HTTP/1.1
<html>
 <head>...</head>
 <body>
   <h1>Wonderfull site!</h1>
   <p>Hello</p>
 </body>
</html>
GET /users/ HTTP/1.1
<html>
 <head>...</head>
 <body>
   <h1>Wonderfull site!</h1>
   <ul>
     <li>Arthur</li>
     <li>Ford</li>
   </ul>
 </body>
</html>

Need to reload the entire page!

Json response

GET /userlist/ HTTP/1.1
Accept: application/json

Json response

HTTP/1.1 200 OK
Content-Length: 130

[ { "name": "Marvin",
    "kind": "Robot" },
  { "name": "Zaphod",
    "kind": "ET"},
  { "name": "Arthur",
    "kind": "human"}
]
GET /userlist/ HTTP/1.1
Accept: application/json

Json response

HTTP/1.1 200 OK
Content-Length: 130

[ { "name": "Marvin",
    "kind": "Robot" },
  { "name": "Zaphod",
    "kind": "ET"},
  { "name": "Arthur",
    "kind": "human"}
]

Display those data with JS (via Elm).

 

"AJAX"

GET /userlist/ HTTP/1.1
Accept: application/json

Dessine moi un mouton !

A

B

C

D

E

F

G

H

I

JSON

JavaScript Object Notation

JSON

JavaScript Object Notation

Only data, not tied to JavaScript

Primitives

Numbers

15
15.2
-54
5e10

Primitives

Numbers

Strings
(including UTF-8)

""
"Hello world"
"😊"
15
15.2
-54
5e10

Primitives

Numbers

15
15.2
-54
5e10

Boolean

true
false

Strings
(including UTF-8)

""
"Hello world"
"😊"

Primitives

Numbers

15
15.2
-54
5e10

Strings
(including UTF-8)

""
"Hello world"
"😊"

Boolean

true
false

Null
(represents empty value)

null

Composites

Arrays

[5, 42, "hello", null]

Composites

Arrays

[5, 42, "hello", null]

Objects

{ "name": "Arthur",
  "kind": "human"
}
  

Composites

[5, 42, "hello", null]


[ { "name": "Marvin",
    "kind": "Robot" },
  { "name": "Zaphod",
    "kind": "ET"},
  { "name": "Arthur",
    "kind": "human"}
]
{ "name": "Arthur",
  "kind": "human"
}

{ "server": "Heart-of-gold",
  "arenas": [
  { "game": "pong",
    "players": [
      { "name": "Arthur",
        "score": 5 },
      { "name": "Zaphod",
        "score": 2 }
    ]
  }
 ]
}   

Arrays

Objects

Convert JSON to Elm

Convert JSON to Elm

can fail!

"Maybe" represents failure

Produce a Maybe

getDay : Int -> Maybe String
getDay dayNumber = case dayNumber of
    0 -> Just "Monday"

    1 -> Just "Tuesday"

    ...

    6 -> Just "Sunday"

    _ -> Nothing

Deal with maybe!

res : Int
res = case (String.toInt "42") of
    Just x ->
        2 * x

    Nothing ->
        0


"Maybe" represents failure

"Result" represents failure
...

 

"Result" represents failure
with an error message

Result

type Result error value
    = Err error
    | Ok value

Result

type Result error value
    = Err error
    | Ok value

Can be any type

Result

type Result error value
    = Err error
    | Ok value
type Maybe a
    = Nothing
    | Just a

Result

type Result error value
    = Err error
    | Ok value
type Maybe a
    = Nothing
    | Just a

Result

type Result error value
    = Err error
    | Ok value


toBool : String -> Result String Bool
toBool s = case s of
    "true"  -> Ok True
    "false" -> Ok False
    "42"    -> Err "Nice try!"
    _       -> Err ("not a bool: " ++ s)

Result

type Result error value
    = Err error
    | Ok value


toBool : String -> Result String Bool
toBool s = case s of
    "true"  -> Ok True
    "false" -> Ok False
    "42"    -> Err "Nice try!"
    _       -> Err ("not a bool: " ++ s)

Error messages,
impossible with Maybe

Result

type Result error value
    = Err error
    | Ok value


toBool : String -> Result String Bool
toBool s = case s of
    "true"  -> Ok True
    "false" -> Ok False
    "42"    -> Err "Nice try!"
    _       -> Err ("not a bool: " ++ s)

Result

type Result error value
    = Err error
    | Ok value


toBool : String -> Result String Bool
toBool s = case s of
    "true"  -> Ok True
    "false" -> Ok False
    "42"    -> Err "Nice try!"
    _       -> Err ("not a bool: " ++ s)

Hands on!

Write a function

kineticE : Float -> Float -> Result String Float 
kineticE mass speed =
    ...

using

E_k = \frac{1}{2} mass \times speed^2

with distinct errors if the mass or the speed are negative

https://ellie-app.com/6JZJFSGqFZta1

Convert JSON to Elm

Decoders

describe what to do
but do nothing

import Json.Decode as Decode exposing (int)

Decode.decodeString int "4"     == Ok 4

Decode.decodeString int "hello" == Err ...

Decode.decodeString int "1 + 2" == Err ...
Decode.decodeString :
    Decoder val
    -> String
    -> Result Error val
Decode.decodeString :
    Decoder val
    -> String
    -> Result Error val
import Json.Decode as Decode exposing (int)

Decode.decodeString int "4"     == Ok 4

Decode.decodeString int "hello" == Err ...

Decode.decodeString int "1 + 2" == Err ...
Decode.decodeString :
    Decoder val
    -> String
    -> Result Error val

The type we want to decode to

import Json.Decode as Decode exposing (int)

Decode.decodeString int "4"     == Ok 4

Decode.decodeString int "hello" == Err ...

Decode.decodeString int "1 + 2" == Err ...
Decode.decodeString :
    Decoder val
    -> String
    -> Result Error val

The decoder, describing how to transform a JSON value into

an Elm value of type "val"

import Json.Decode as Decode exposing (int)

Decode.decodeString int "4"     == Ok 4

Decode.decodeString int "hello" == Err ...

Decode.decodeString int "1 + 2" == Err ...
Decode.decodeString :
    Decoder val
    -> String
    -> Result Error val

The JSON we want to decode:

  • 42
  • {"name": "Arthur", "age": 42}
  • [5, {"name":"Ford"}]
  • ...
import Json.Decode as Decode exposing (int)

Decode.decodeString int "4"     == Ok 4

Decode.decodeString int "hello" == Err ...

Decode.decodeString int "1 + 2" == Err ...
Decode.decodeString :
    Decoder val
    -> String
    -> Result Error val

Either:

  • the resulting value,
  • or an Error if the decoder failed to decode the JSON
import Json.Decode as Decode exposing (int)

Decode.decodeString int "4"     == Ok 4

Decode.decodeString int "hello" == Err ...

Decode.decodeString int "1 + 2" == Err ...
import Json.Decode as Decode exposing (Decoder, int)

Decode.decodeString int "4"     == Ok 4

Decode.decodeString int "hello" == Err ...

Decode.decodeString int "1 + 2" == Err ...

	int : Decoder Int
Decode.decodeString :
    Decoder val
    -> String
    -> Result Error val
import Json.Decode as Decode exposing (Decoder, int)

Decode.decodeString int "4"     == Ok 4

Decode.decodeString int "hello" == Err ...

Decode.decodeString int "1 + 2" == Err ...

	int : Decoder Int
Decode.decodeString :
    Decoder Int
    -> String
    -> Result Error Int

Decode primitives

import Json.Decode as Decode exposing 
    (Decoder, decodeString)


Decode.int : Decoder Int
decodeString Decode.int "4"     == Ok 4


Decode.string : Decoder String
decodeString Decode.string "\"hey\""           == Ok "hey"
decodeString Decode.string "3.14"              == Err ...
decodeString Decode.string "{ \"hello\": 42 }" == Err ...

Decode.bool : Decoder Bool
decodeString bool "true"              == Ok True
decodeString bool "42"                == Err ...

Decode Arrays

import Json.Decode as Decode exposing 
    (Decoder, decodeString, list, int, bool)


Decode.list : Decoder a -> Decoder (List a)
decodeString (list int) "[1,2,3]"       == Ok [1,2,3]
decodeString (list bool) "[true,false]" == Ok [True,False]

decodeString (list int) "[true,false]" == Err ...
decodeString (list bool) "[1,2,3]" == Err ...

decodeString (list int) "42" == Err ...

Hands on 1

Write a decoder for the following JSON:

42

Hands on 2

Write a decoder for the following JSON:

[42, 10, 52]

Hands on 3

Write a decoder for the following JSON:

[[1, 2, 3], [42, 6, 9], [5,1]]

Decode Field in Object

import Json.Decode as Decode exposing 
    (Decoder, decodeString, field, int, string)


json = "{ \"username\": \"Tom\", \"age\": 42 }" 

Decode.field : String -> Decoder a -> Decoder a
decodeString (field "username" string) json == Ok "Tom"
decodeString (field "age" int) json         == Ok 42

decodeString (field "age" string) json      == Err ...
decodeString (field "username" int) json    == Err ...

Hands on 4

Get the score from this JSON:

{ "name": "Ford", "score": 42 }

Decode Objects

import Json.Decode as Decode exposing 
    (Decoder, decodeString, field, int, string, map2)


json = "{ \"username\": \"Tom\", \"age\": 42 }" 

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

userDecoder : Decoder User
userDecoder =
    map2 User
        (field "username" string)
        (field "age" int)

decodeString userDecoder json 
        == Ok { name = "Tom", age = 42 }

Decode Objects

import Json.Decode as Decode exposing 
    (Decoder, decodeString, field, int, string, map2)


json = "{ \"username\": \"Tom\", \"age\": 42 }" 

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

userDecoder : Decoder User
userDecoder =
    map2 User
        (field "username" string)
        (field "age" int)

decodeString userDecoder json 
        == Ok { name = "Tom", age = 42 }

⚠️

Order of the fields matters!

import Json.Decode as Decode

Decode.decodeString userDecoder
   """{ "username": "Tom", "age": 42, "city": "NY" }"""

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

userDecoder: Decoder User
-- produces:
Ok { name : "Tom", age : 42 }
  • Field names are decoupled in JSON and Elm
    (the matching is done in the decoder)
  • The decoder can "ignore" some data
import Json.Decode as Decode

Decode.decodeString userDecoder
   """{ "username": "Tom", "age": 42, "city": "NY" }"""




Decode.decodeString userDecoder
   """{ "username": "Tom", "nbYears": 42, "city": "NY" }"""
-- produces:
Ok { name : "Tom", age : 42 }
-- produces:
Err ...

Hands on 5

Decode the JSON on the left to the type on the right:

{ 
  "name": "Ford",
  "score": 42,
  "favorite_color": "red",
  "from": "Betelgeuse"
}
type alias Player =
  { name : String
  , score: Int
  , origin: String
  }

Hands on 6

Decode the JSON on the left to the type on the right:

{
  "players": [{ 
    "name": "Ford",
    "score": 42,
    "favorite_color": "red",
    "from": "Betelgeuse"
  }],
  "games": ["pong", "tetris"]
}
type alias Board =
  { players: List Player
  , games : List String
  }
  
-- Player is the same
-- as previously
initialModel : Model
initialModel =
    { count = 0 }

update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Increment ] [ text "+1" ]
        , text <| String.fromInt model.count
        ]
init : Model

view : Model -> Html Msg

update : Msg -> Model -> Model

Json response

GET /userlist/ HTTP/1.1
Accept: application/json
HTTP/1.1 200 OK
Content-Length: 130

[ { "name": "Marvin",
    "kind": "Robot" },
  { "name": "Zaphod",
    "kind": "ET"},
  { "name": "Arthur",
    "kind": "human"}
]
init : Model

view : Model -> Html Msg

update : Msg -> Model -> Model

No side
effect!

init : flags -> (Model, Cmd Msg)

view : Model -> Html Msg

update : Msg -> Model -> (Model, Cmd Msg)

1. Hey Runtime,

please perform an HTTP request!

3. Ok, here is the response

2.

1. A new model indicating "Loading"

4. A new model with loaded data

HTTP request can fail

HTTP request can fail

because:

  • the server is down,

  • the network is down,

  • Obiwan Kenobi,

  • ...

Http Error

type Result error value
    = Err error
    | Ok value


-- in the module Http
type Error
    = BadUrl String
    | Timeout
    | NetworkError
    | BadStatus Int
    | BadBody String
showResp : Result Http.Error (List User) 
         -> String
showResp resp =
    case resp of
        Ok users ->
            showUsers users

        Err _ ->
            "An error occured, I'm sory!"
showResp : Result Http.Error (List User) 
         -> String
showResp resp =
    case resp of
        Ok users ->
            showUsers users

        Err _ ->
            "An error occured, I'm sory!"
showResp : Result Http.Error (List User) 
         -> String
showResp resp =
    case resp of
        Ok users ->
            showUsers users

        Err _ ->
            "An error occured, I'm sory!"
showResp : Result Http.Error (List User) 
         -> String
showResp resp =
    case resp of
        Ok users ->
            showUsers users

        Err Http.NetworkError ->
            "You seem disconnected."
    
        Err Http.Timeout ->
            "The server is too slow."

        Err _ ->
            "An error occured, I'm sory!"

Json response

GET /userlist/ HTTP/1.1
Accept: application/json
HTTP/1.1 200 OK
Content-Length: 130

[ { "name": "Marvin",
    "kind": "Robot" },
  { "name": "Zaphod",
    "kind": "ET"},
  { "name": "Arthur",
    "kind": "human"}
]
type Msg
    = LoadUserClicked
    | GotUsers (Result Http.Error (List User))
<<Hey Runtime! Fetch data from "/userlist/" and
  when you are done, issue a 'GotUsers'
  message with the result and convert the data
  with 'usersDecoder'!>>
type Msg
    = LoadUserClicked
    | GotUsers (Result Http.Error (List User))
Cmd Msg
<<Hey Runtime! Fetch data from "/userlist/" and
  when you are done, issue a 'GotUsers'
  message with the result and convert the data
  with 'usersDecoder'!>>
type Msg
    = LoadUserClicked
    | GotUsers (Result Http.Error (List User))
Cmd Msg
Msg
Json decoder
<<Hey Runtime! Fetch data from "/userlist/" and
  when you are done, issue a 'GotUsers'
  message with the result and convert the data
  with 'usersDecoder'!>>
type Msg
    = LoadUserClicked
    | GotUsers (Result Http.Error (List User))
init : flags -> (Model, Cmd Msg)

view : Model -> Html Msg

update : Msg -> Model -> (Model, Cmd Msg)
Cmd Msg
Msg
Http.get { url = "/userlist/"
    , expect = Http.expectJson 
    	GotUsers
        usersDecoder
    }
Json decoder
type Msg
    = LoadUserClicked
    | GotUsers (Result Http.Error (List User))
type Msg
    = GotUsers (Result Http.Error (List User))
    | LoadUserClicked
    
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    LoadUserClicked ->
      ( model -- model not changed
      , Http.get
         { url = "/userlist/"
         , expect = Http.expectJson GotUsers usersDecoder
         }
      )
    
    GotUsers result ->
      ( { model | users = result }
      , Cmd.none -- no command to perform!
      )

Json and Elm decoders - TWTS 04

By sebbes

Json and Elm decoders - TWTS 04

  • 831