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 as 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 Lists. 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, 4]) = 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 that may or may not be present. In other environment these might be called “nullable” values.

Like the list decoder, the optional decoder 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 choose 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 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 decoder for any other strings
      _ -> 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 returns when run.

pub type DecodeResult(t) =
  Result(t, List(dynamic.DecodeError))

A decoder is a value that can be used to turn dynamically typed Dynamic data into typed data using the from function.

Several smaller decoders can be combined to make larger decoders using functions such as list and field.

pub opaque type Decoder(t)

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.14))
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)
pub const string: Decoder(String)

A decoder that decodes String values.

Examples

decode.string
|> decode.from(dynamic.from("Hello!"))
// -> Ok("Hello!")

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 can handle 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"))
pub fn then(
  decoder: Decoder(a),
  next: fn(a) -> Decoder(b),
) -> Decoder(b)

Create a new decoder based upon the value of a previous decoder.

This may be useful for when you need to know some of the structure of the dynamic value in order to know how to decode the rest of it.

Search Document