decode
The Dynamic
type is used to represent dynamically typed data. That is, data
that we don’t know the precise type of yet, so we need to introspect the data to
see if it is of the desired type before we can use it. Typically data like this
would come from user input or from untyped languages such Erlang or JavaScript.
This module provides the Decoder
type and associated functions, which provides
a type safe and composable way to convert dynamic data into some desired type,
or into errors if the data doesn’t have the desired structure.
The Decoder
type is generic and has 1 type parameter, which is the type that
it attempts to decode. A Decoder(String)
can be used to decode strings, and a
Decoder(Option(Int))
can be used to decode Option(Int)
s
Decoders work using runtime reflection and the data structures of the target platform. Differences between Erlang and JavaScript data structures may impact your decoders, so it is important to test your decoders on all supported platforms.
Examples
Dynamic data may come from various sources and so many different syntaxes could be used to describe or construct them. In these examples the JSON syntax is largely used, and you can apply the same techniques to data from any source.
Simple types
This module defines decoders for simple data types such as string
,
int
, float
, bit_array
, and bool
.
// Data:
// "Hello, Joe!"
let result =
decode.string
|> decode.from(data)
let assert Ok("Hello, Joe!") = result
Lists
The list
decoder decodes List
s. To use it you must construct it by
passing in another decoder into the list
function, which is the decoder that
is to be used for the elements of the list, type checking both the list and its
elements.
// Data:
// [1, 2, 3, 4]
let result =
decode.list(decode.int)
|> decode.from(data)
let assert Ok([1, 2, 3]) = result
On Erlang this decoder can decode from lists, and on JavaScript it can decode from lists as well as JavaScript arrays.
Options
The optional
decoder is used to decode values with may or may not
be present. In other environment these might be called “nullable” values.
Like the list
decoder in that is takes another decoder, which is used to decode
the value if it is present.
// Data:
// 12.45
let result =
decode.optional(decode.int)
|> decode.from(data)
let assert Ok(option.Some(12.45)) = result
// Data:
// null
let result =
decode.optional(decode.int)
|> decode.from(data)
let assert Ok(option.None) = result
This decoder knows how to handle multiple different runtime representations of
absent values, including Nil
, None
, null
, and undefined
.
Dicts
The dict
decoder decodes Dicts
and contains two other decoders, one
for the keys, one for the values.
// Data:
// { "Lucy": 10, "Nubi": 20 }
let result =
decode.dict(decode.string, decode.int)
|> decode.from(data)
let assert Ok(dict.from_list([#("Lucy", 10), #("Nubi": 20)])) = result
Indexing objects
The at
decoder can be used to decode a value that is nested within
key-value containers such as Gleam dicts, Erlang maps, or JavaScript objects.
// Data:
// { "one": { "two": 123 } }
let result =
decode.at(["one", "two"], decode.int)
|> decode.from(data)
let assert Ok(123) = result
Indexing arrays
If you use ints as keys then the at
decoder can be used to index into
array-like containers such as Gleam or Erlang tuples, or JavaScript arrays.
// Data:
// ["one", "two", "three"]
let result =
decode.at([1], decode.string)
|> decode.from(data)
let assert Ok("two") = result
Records
Decoding records from dynamic data is more complex and requires combining a decoder for each field and a special constructor that builds your records with the decoded field values.
// Data:
// {
// "score": 180,
// "name": "Mel Smith",
// "is-admin": false,
// "enrolled": true,
// "colour": "Red",
// }
let result =
decode.into({
use name <- decode.parameter
use score <- decode.parameter
use colour <- decode.parameter
use enrolled <- decode.parameter
Player(name: name, score: score, colour: colour, enrolled: enrolled)
})
|> decode.field("name", decode.string)
|> decode.field("score", decode.int)
|> decode.field("colour", decode.string)
|> decode.field("enrolled", decode.bool)
|> decode.from(data)
let assert Ok(Player("Mel Smith", 180, "Red", True)) = result
The ordering of the parameters defined with the parameter
function must match
the ordering of the decoders used with the field
function.
Enum variants
Imagine you have a custom type where all the variants do not contain any values.
pub type PocketMonsterType {
Fire
Water
Grass
Electric
}
You might chose to encode these variants as strings, "fire"
for Fire
,
"water"
for Water
, and so on. To decode them you’ll need to decode the dynamic
data as a string, but then you’ll need to decode it further still as not all
strings are valid values for the enum. This can be done with the then
function, which enables running of a second decoder after the first one
succeeds.
let decoder =
decode.string
|> decode.then(fn(decoded_string) {
case decoded_string {
// Return succeeding decoders for valid strings
"fire" -> decode.into(Fire)
"water" -> decode.into(Water)
"grass" -> decode.into(Grass)
"electric" -> decode.into(Electric)
// Return a failing decoders for any other string
_ -> decode.fail("PocketMonsterType")
}
})
decoder
|> decode.from(dynamic.from("water"))
// -> Ok(Water)
decoder
|> decode.from(dynamic.from("wobble"))
// -> Error([DecodeError("PocketMonsterType", "String", [])])
Record variants
Decoding type variants that contain other values is done by combining the techniques from the “enum variants” and “records” examples. Imagine you have this custom type that you want to decode:
pub type PocketMonsterPerson {
Trainer(name: String, badge_count: Int)
GymLeader(name: String, speciality: PocketMonsterType)
}
And you would like to be able to decode these from JSON documents like these.
{
"type": "trainer",
"name": "Ash",
"badge-count": 1,
}
{
"type": "gym-leader",
"name": "Misty",
"speciality": "water",
}
Notice how both documents have a "type"
field, which is used to indicate which
variant the data is for.
First, define decoders for each of the variants:
let trainer_decoder =
decode.into({
use name <- decode.parameter
use badge_count <- decode.parameter
Trainer(name, badge_count)
})
|> decode.field("name", decode.string)
|> decode.field("badge-count", decode.int)
let gym_leader_decoder =
decode.into({
use name <- decode.parameter
use speciality <- decode.parameter
GymLeader(name, speciality)
})
|> decode.field("name", decode.string)
|> decode.field("speciality", pocket_monster_type_decoder)
A third decoder can be used to extract and decode the "type"
field, and the
then
function then returns whichever decoder is suitable for the document.
let decoder =
decode.at(["type"], decode.string)
|> decode.then(fn(tag) {
case tag {
"trainer" -> trainer_decoder
"gym-leader" -> gym_leader
_ -> decode.fail("PocketMonsterPerson")
}
})
decoder
|> decode.from(data)
Types
The result that a decoder runs when run.
pub type DecodeResult(t) =
Result(t, List(dynamic.DecodeError))
Constants
pub const bit_array: Decoder(BitArray)
A decoder that decodes BitArray
values. This decoder never returns an error.
Examples
decode.bit_array
|> decode.from(dynamic.from(<<5, 7>>))
// -> Ok(<<5, 7>>)
pub const bool: Decoder(Bool)
A decoder that decodes Bool
values.
Examples
decode.bool
|> decode.from(dynamic.from(True))
// -> Ok(True)
pub const dynamic: Decoder(Dynamic)
A decoder that decodes Dynamic
values. This decoder never returns an error.
Examples
decode.dynamic
|> decode.from(dynamic.from(3.14))
// -> Ok(dynamic.from(3.13))
pub const float: Decoder(Float)
A decoder that decodes Float
values.
Examples
decode.float
|> decode.from(dynamic.from(3.14))
// -> Ok(3.14)
pub const int: Decoder(Int)
A decoder that decodes Int
values.
Examples
decode.int
|> decode.from(dynamic.from(147))
// -> Ok(147)
Functions
pub fn at(path: List(a), inner: Decoder(b)) -> Decoder(b)
A decoder that decodes a value that is nested within other values. For example, decoding a value that is within some deeply nested JSON objects.
This function will index into dictionaries with any key type, and if the key is an int then it’ll also index into Erlang tuples and JavaScript arrays.
Examples
let data = dynamic.from(dict.from_list([
#("one", dict.from_list([
#("two", 1000),
])),
]))
decode.at(["one", "two"], decode.int)
|> decode.from(data)
// -> Ok(1000)
decode.optional(of: decode.int)
|> decode.from(dynamic.from(Nil))
// -> Ok(option.None)
pub fn collapse_errors(
decoder: Decoder(a),
name: String,
) -> Decoder(a)
Replace all errors produced by a decoder with one single error for a named expected type.
This function may be useful if you wish to simplify errors before
presenting them to a user, particularly when using the one_of
function.
Examples
decode.string
|> decode.collapse_errors("MyThing")
|> decode.from(dynamic.from(1000))
// -> Error([DecodeError("MyThing", "Int", [])])
pub fn dict(
key: Decoder(a),
value: Decoder(b),
) -> Decoder(Dict(a, b))
A decoder that decodes dicts where all keys and vales are decoded with given decoders.
Examples
let values = dict.from_list([
#("one", 1),
#("two", 2),
])
decode.dict(decode.string, decode.int)
|> decode.from(dynamic.from(values))
// -> Ok(values)
pub fn fail(expected: String) -> Decoder(a)
Define a decoder that always fails. The parameter for this function is the name of the type that has failed to decode.
pub fn field(
decoder: Decoder(fn(a) -> b),
field_name: c,
field_decoder: Decoder(a),
) -> Decoder(b)
Run a decoder on a Dynamic
value, decoding the value if it is of the
desired type, or returning errors.
The second parameter is a field name which will be used to index into the
Dynamic
data.
This function will index into dictionaries with any key type, and if the key is
an int then it’ll also index into Erlang tuples and JavaScript arrays.
Examples
let data = dynamic.from(dict.from_list([
#("email", "lucy@example.com"),
#("name", "Lucy"),
]))
decode.into({
use name <- decode.parameter
use email <- decode.parameter
SignUp(name: name, email: email)
})
|> decode.field("name", string)
|> decode.field("email", string)
|> decode.from(data)
// -> Ok(SignUp(name: "Lucy", email: "lucy@example.com"))
If you wish to decode a value that is more deeply nested within the dynamic
data see subfield
and at
.
pub fn from(
decoder: Decoder(a),
data: Dynamic,
) -> Result(a, List(DecodeError))
Run a decoder on a Dynamic
value, decoding the value if it is of the
desired type, or returning errors.
Examples
decode.into({
use name <- decode.parameter
use email <- decode.parameter
SignUp(name: name, email: email)
})
|> decode.field("email", string)
|> decode.field("password", string)
|> decode.from(data)
pub fn into(constructor: a) -> Decoder(a)
Create a new decoder for a given constructor function. If this function is
a function that takes parameters one-at-a-time, such as anonymous functions
made with use name <- decode.parameter
then the decoder can be used with
the decode.field
function to decode a value that contains multiple other
values.
Examples
let data = dynamic.from(dict.from_list([
#("email", "lucy@example.com"),
#("name", "Lucy"),
]))
decode.into({
use name <- decode.parameter
use email <- decode.parameter
SignUp(name: name, email: email)
})
|> decode.field("name", string)
|> decode.field("email", string)
|> decode.from(data)
// -> Ok(SignUp(name: "Lucy", email: "lucy@example.com"))
pub fn list(of item: Decoder(a)) -> Decoder(List(a))
A decoder that decodes lists where all elements are decoded with a given decoder.
Examples
decode.list(of: decode.int)
|> decode.from(dynamic.from([1, 2, 3]))
// -> Ok([1, 2, 3])
pub fn map(
decoder: Decoder(a),
transformer: fn(a) -> b,
) -> Decoder(b)
Apply a transformation function to any value decoded by the decoder.
Examples
decode.int
|> decode.map(int.to_string)
|> decode.from(dynamic.from(1000))
// -> Ok("1000")
pub fn map_errors(
decoder: Decoder(a),
transformer: fn(List(DecodeError)) -> List(DecodeError),
) -> Decoder(a)
Apply a transformation function to any errors returned by the decoder.
pub fn one_of(decoders: List(Decoder(a))) -> Decoder(a)
Create a new decoder from several other decoders. Each of the inner decoders is run in turn, and the value from the first to succeed is used.
If no decoder succeeds then the errors from the final decoder is used.
If you wish for different errors then you may wish to use the
collapse_errors
or map_errors
functions.
Examples
decode.one_of([
decode.string,
decode.int |> decode.map(int.to_string),
])
|> decode.from(dynamic.from(1000))
// -> Ok("1000")
pub fn optional(item: Decoder(a)) -> Decoder(Option(a))
A decoder that decodes nullable values of a type decoded by with a given decoder.
This function common representations of null on all runtimes, such as
nil
, null
, and undefined
on Erlang, and undefined
and null
on
JavaScript.
Examples
decode.optional(of: decode.int)
|> decode.from(dynamic.from(100))
// -> Ok(option.Some(100))
decode.optional(of: decode.int)
|> decode.from(dynamic.from(Nil))
// -> Ok(option.None)
pub fn parameter(body: fn(a) -> b) -> fn(a) -> b
This function is used to create constructor functions that take arguments
one at a time, making them suitable for passing to the into
function.
Examples
let data = dynamic.from(dict.from_list([
#("email", "lucy@example.com"),
#("name", "Lucy"),
]))
decode.into({
use name <- decode.parameter
use email <- decode.parameter
SignUp(name: name, email: email)
})
|> decode.field("name", string)
|> decode.field("email", string)
|> decode.from(data)
// -> Ok(SignUp(name: "Lucy", email: "lucy@example.com"))
pub fn subfield(
decoder: Decoder(fn(a) -> b),
field_path: List(c),
field_decoder: Decoder(a),
) -> Decoder(b)
The same as field
, except taking a path to the value rather
than a field name.
Examples
let data = dynamic.from(dict.from_list([
#("data", data.from_list([
#("email", "lucy@example.com"),
#("name", "Lucy"),
]))
]))
decode.into({
use name <- decode.parameter
use email <- decode.parameter
SignUp(name: name, email: email)
})
|> decode.subfield(["data", "name"], string)
|> decode.subfield(["data", "email"], string)
|> decode.from(data)
// -> Ok(SignUp(name: "Lucy", email: "lucy@example.com"))