To show how this package works, we'll go through these steps:
field
map
andThen
require
<|
Decode.field
decodes a particular field from a JSON object:
usernameDecoder : Decoder String
usernameDecoder =
Decode.field "username" Decode.string
This decoder will decode the string "rtfeldman"
from the following JSON:
{"id": 5, "username": "rtfeldman", "name": "Richard Feldman"}
However, this decoder would fail if any of the following were true:
- It was not run on a JSON object
- The object did not have a field called
username
- The
username
field was not a string
List.map
uses a function to transform each value inside a list:
List.map String.toLower [ "A", "B", "C" ]
--> [ "a", "b", "c" ]
Decode.map
uses a function to transform a successfully decoded value:
Decode.map String.toLower Decode.string
This code returns a Decoder String
which decodes a string, and then lowercases
it. If decoding failed (for example because it tried to run this decoder on a
number instead of a string), then String.toLower
would not get called.
andThen
works like map
except the transformation function has the power to
change successes into failures.
andThen
lets us perform validation in a Decoder
. For example, here we'll
take a string and then:
- Check if it's empty. If it's an empty string, fail decoding.
- If it's not empty, lowercase it.
validateAndTransform : String -> Decoder String
validateAndTransform str =
if String.isEmpty str then
Decode.fail "the string was empty"
else
Decode.succeed (String.toLower str)
decoder : Decoder String
decoder =
Decode.andThen validateAndTransform Decode.string
require
is a convenience function which does a Decode.field
followed by a Decode.andThen
.
lowercaseUsernameDecoder : Decoder String
lowercaseUsernameDecoder =
require "username" Decode.string (\str ->
if String.isEmpty str then
Decode.fail "the string was empty"
else
Decode.succeed (String.toLower str)
)
We can run this decoder on the following JSON:
{"id": 5, "username": "RTFELDMAN", "name": "Richard Feldman"}
It will give back "rtfeldman"
because it lowercases the successfully decoded
"RTFELDMAN"
.
This decoder would fail if any of the following were true:
- It was not run on a JSON object
- The object did not have a field called
username
- The
username
field was not a string - The
username
field was present, and a string, but the string was empty
We can chain several require
calls together to decode into a record.
type alias User =
{ id : Int, username : String, name : String }
userDecoder : Decoder User
userDecoder =
require "username" string (\username ->
require "id" int (\id ->
require "name" string (\name ->
succeed { id = id, username = username, name = name }
)
)
)
If any of these require
calls fails to decode, the whole decoder will fail,
because require
uses andThen
under the hood - meaning its transformation
function can return a failure outcome.
If they all succeed, the innermost transformation function will be run. It
already has id
, name,
and username
in scope, so it can use them to
Decode.succeed
with a User
record.
We can make this read more like a schema if we use <|
. The <|
operator can
take the place of parentheses. These two lines of code do exactly the same thing:
String.toLower (getStr something)
String.toLower <| getStr something
<|
expects a function on the left, and calls that function passing the value
on the right. We can use this to write the above decoder without so many parentheses:
userDecoder : Decoder User
userDecoder =
require "username" string <| \username ->
require "id" int <| \id ->
require "name" string <| \name ->
succeed { id = id, username = username, name = name }
This way, the sequence of require
calls can be read like a schema for the JSON.