json/blueprint

The 1.x API, frozen: one-way Decoder(t) values that decode JSON text into Gleam values and describe that JSON as a schema document labelled Draft-07, plus encoders for unions, enums, optional fields and tuples.

Use json/blueprint/codec for new code: one Codec(a) encodes, decodes strictly and describes a Draft 2020-12 schema. Keep this module for recursive types, which self_decoder and reuse_decoder express with $ref and $defs, and for existing 1.x code. Its wire formats differ from codec: unions use {"type", "data"}, enums encode as {"enum": label}, and objects ignore unknown fields. The schema types live in json/blueprint/schema.

decode parses with gleam/json and rejects text above 1 MiB before parsing; decode_with_max_bytes accepts another bound.

import gleam/json
import json/blueprint

pub type User {
  User(name: String, age: Int)
}

pub fn user_decoder() -> blueprint.Decoder(User) {
  blueprint.decode2(
    User,
    blueprint.field("name", blueprint.string()),
    blueprint.field("age", blueprint.int()),
  )
}

pub fn example() {
  let assert Ok(User("Ada", 36)) =
    blueprint.decode(
      using: user_decoder(),
      from: "{\"name\":\"Ada\",\"age\":36}",
    )
  blueprint.generate_json_schema(user_decoder()) |> json.to_string
}

Types

pub opaque type Decoder(t)
pub opaque type FieldDecoder(t)
pub type LazyDecoder(t) =
  fn() -> Decoder(t)

Values

pub fn bool() -> Decoder(Bool)
pub fn decode(
  using decoder: Decoder(t),
  from json_string: String,
) -> Result(t, json.DecodeError)

Parse JSON text with gleam/json and decode it with decoder.

Text above 1 MiB (1,048,576 bytes of UTF-8) is rejected before parsing with json.UnableToDecode; its one error has an empty path, expected names the limit and found is "more than <limit> bytes". Use decode_with_max_bytes to accept larger text.

pub fn decode0(constructor: t) -> Decoder(t)

Return the constructor for any input, including a non-object value. The legacy schema describes an empty closed object, but this decoder does not enforce that description.

pub fn decode1(
  constructor: fn(t1) -> t,
  t1: FieldDecoder(t1),
) -> Decoder(t)
pub fn decode2(
  constructor: fn(t1, t2) -> t,
  t1: FieldDecoder(t1),
  t2: FieldDecoder(t2),
) -> Decoder(t)
pub fn decode3(
  constructor: fn(t1, t2, t3) -> t,
  t1: FieldDecoder(t1),
  t2: FieldDecoder(t2),
  t3: FieldDecoder(t3),
) -> Decoder(t)
pub fn decode4(
  constructor: fn(t1, t2, t3, t4) -> t,
  t1: FieldDecoder(t1),
  t2: FieldDecoder(t2),
  t3: FieldDecoder(t3),
  t4: FieldDecoder(t4),
) -> Decoder(t)
pub fn decode5(
  constructor: fn(t1, t2, t3, t4, t5) -> t,
  t1: FieldDecoder(t1),
  t2: FieldDecoder(t2),
  t3: FieldDecoder(t3),
  t4: FieldDecoder(t4),
  t5: FieldDecoder(t5),
) -> Decoder(t)
pub fn decode6(
  constructor: fn(t1, t2, t3, t4, t5, t6) -> t,
  t1: FieldDecoder(t1),
  t2: FieldDecoder(t2),
  t3: FieldDecoder(t3),
  t4: FieldDecoder(t4),
  t5: FieldDecoder(t5),
  t6: FieldDecoder(t6),
) -> Decoder(t)
pub fn decode7(
  constructor: fn(t1, t2, t3, t4, t5, t6, t7) -> t,
  t1: FieldDecoder(t1),
  t2: FieldDecoder(t2),
  t3: FieldDecoder(t3),
  t4: FieldDecoder(t4),
  t5: FieldDecoder(t5),
  t6: FieldDecoder(t6),
  t7: FieldDecoder(t7),
) -> Decoder(t)
pub fn decode8(
  constructor: fn(t1, t2, t3, t4, t5, t6, t7, t8) -> t,
  t1: FieldDecoder(t1),
  t2: FieldDecoder(t2),
  t3: FieldDecoder(t3),
  t4: FieldDecoder(t4),
  t5: FieldDecoder(t5),
  t6: FieldDecoder(t6),
  t7: FieldDecoder(t7),
  t8: FieldDecoder(t8),
) -> Decoder(t)
pub fn decode9(
  constructor: fn(t1, t2, t3, t4, t5, t6, t7, t8, t9) -> t,
  t1: FieldDecoder(t1),
  t2: FieldDecoder(t2),
  t3: FieldDecoder(t3),
  t4: FieldDecoder(t4),
  t5: FieldDecoder(t5),
  t6: FieldDecoder(t6),
  t7: FieldDecoder(t7),
  t8: FieldDecoder(t8),
  t9: FieldDecoder(t9),
) -> Decoder(t)
pub fn decode_with_max_bytes(
  using decoder: Decoder(t),
  from json_string: String,
  max_bytes max_bytes: Int,
) -> Result(t, json.DecodeError)

Like decode, but reject text above max_bytes bytes of UTF-8 instead of the 1 MiB default. A max_bytes below 1 rejects every non-empty text.

pub fn encode_optional_field(
  fields fields: List(#(String, json.Json)),
  name name: String,
  maybe value: option.Option(t),
  encoder encode_fn: fn(t) -> json.Json,
) -> List(#(String, json.Json))

Prepend an encoded field for Some, or omit it for None. Pair this encoder with optional_field when absence represents None.

Example

fn tree_decoder() {
  blueprint.union_type_decoder([
    #(
      "node",
      blueprint.decode3(
        Node,
        blueprint.field("value", blueprint.int()),
        // This is an optional field for an optional value
        blueprint.optional_field("left", blueprint.self_decoder(tree_decoder)),
        // And this is a required field with an optional value
        blueprint.field(
          "right",
          blueprint.optional(blueprint.self_decoder(tree_decoder)),
        ),
      ),
    ),
  ])
  |> blueprint.reuse_decoder
}

fn encode_tree(tree: Tree) -> json.Json {
  blueprint.union_type_encoder(tree, fn(node) {
    case node {
      Node(value, left, right) -> #(
        "node",
        [
          #("value", json.int(value)),
          // This is a required field with an optional value
          #("right", json.nullable(right, encode_tree)),
        ]
          // And this is an optional field for an optional value
          |> blueprint.encode_optional_field("left", left, encode_tree)
          |> json.object(),
      )
    }
  })
}
pub fn encode_tuple2(
  tuple tuple: #(a, b),
  first encode1: fn(a) -> json.Json,
  second encode2: fn(b) -> json.Json,
) -> json.Json
pub fn encode_tuple3(
  tuple tuple: #(a, b, c),
  first encode1: fn(a) -> json.Json,
  second encode2: fn(b) -> json.Json,
  third encode3: fn(c) -> json.Json,
) -> json.Json
pub fn encode_tuple4(
  tuple tuple: #(a, b, c, d),
  first encode1: fn(a) -> json.Json,
  second encode2: fn(b) -> json.Json,
  third encode3: fn(c) -> json.Json,
  fourth encode4: fn(d) -> json.Json,
) -> json.Json
pub fn encode_tuple5(
  tuple tuple: #(a, b, c, d, e),
  first encode1: fn(a) -> json.Json,
  second encode2: fn(b) -> json.Json,
  third encode3: fn(c) -> json.Json,
  fourth encode4: fn(d) -> json.Json,
  fifth encode5: fn(e) -> json.Json,
) -> json.Json
pub fn encode_tuple6(
  tuple tuple: #(a, b, c, d, e, f),
  first encode1: fn(a) -> json.Json,
  second encode2: fn(b) -> json.Json,
  third encode3: fn(c) -> json.Json,
  fourth encode4: fn(d) -> json.Json,
  fifth encode5: fn(e) -> json.Json,
  sixth encode6: fn(f) -> json.Json,
) -> json.Json
pub fn encode_tuple7(
  tuple tuple: #(a, b, c, d, e, f, g),
  first encode1: fn(a) -> json.Json,
  second encode2: fn(b) -> json.Json,
  third encode3: fn(c) -> json.Json,
  fourth encode4: fn(d) -> json.Json,
  fifth encode5: fn(e) -> json.Json,
  sixth encode6: fn(f) -> json.Json,
  seventh encode7: fn(g) -> json.Json,
) -> json.Json
pub fn encode_tuple8(
  tuple tuple: #(a, b, c, d, e, f, g, h),
  first encode1: fn(a) -> json.Json,
  second encode2: fn(b) -> json.Json,
  third encode3: fn(c) -> json.Json,
  fourth encode4: fn(d) -> json.Json,
  fifth encode5: fn(e) -> json.Json,
  sixth encode6: fn(f) -> json.Json,
  seventh encode7: fn(g) -> json.Json,
  eighth encode8: fn(h) -> json.Json,
) -> json.Json
pub fn encode_tuple9(
  tuple tuple: #(a, b, c, d, e, f, g, h, i),
  first encode1: fn(a) -> json.Json,
  second encode2: fn(b) -> json.Json,
  third encode3: fn(c) -> json.Json,
  fourth encode4: fn(d) -> json.Json,
  fifth encode5: fn(e) -> json.Json,
  sixth encode6: fn(f) -> json.Json,
  seventh encode7: fn(g) -> json.Json,
  eighth encode8: fn(h) -> json.Json,
  ninth encode9: fn(i) -> json.Json,
) -> json.Json
pub fn enum_type_decoder(
  constructor_decoders decoders: List(#(String, a)),
) -> Decoder(a)

Decode {"enum": label} using the listed label/value pairs. Test every enum value; the compiler cannot check that this list is exhaustive.

Example

type Color {
  Red
  Green
  Blue
}

let color_decoder = enum_type_decoder([
  #("red", Red),
  #("green", Green),
  #("blue", Blue),
])
pub fn enum_type_encoder(
  value of: a,
  encoder_fn encoder_fn: fn(a) -> String,
) -> json.Json

Encode a constructor with no arguments as {"enum": label}. The callback selects its label; keep labels aligned with enum_type_decoder.

Example

type Color {
  Red
  Green
  Blue
}

let color_encoder = enum_type_encoder(fn(color) {
  case color {
    Red -> "red"
    Green -> "green"
    Blue -> "blue"
  }
})
pub fn field(
  named name: String,
  of inner_type: Decoder(t),
) -> FieldDecoder(t)
pub fn float() -> Decoder(Float)
pub fn generate_json_schema(decoder: Decoder(t)) -> json.Json
pub fn get_dynamic_decoder(
  decoder: Decoder(t),
) -> fn(dynamic.Dynamic) -> Result(t, List(decode.DecodeError))

The decoder as a function over Dynamic data, for use with a parser other than decode. Errors use gleam/dynamic/decode.DecodeError.

pub fn int() -> Decoder(Int)
pub fn list(
  of decoder_type: Decoder(inner),
) -> Decoder(List(inner))
pub fn map(
  decoder decoder: Decoder(a),
  over foo: fn(a) -> b,
) -> Decoder(b)
pub fn optional(
  of decode: Decoder(inner),
) -> Decoder(option.Option(inner))

Decode null as None and another value with the inner decoder as Some. A missing field still fails; use optional_field for absence.

Example

type User {
  User(name: String, age: Option(Int))
}

let decoder = decode2(
  User,
  field("name", string()),
  field("age", optional(int()))  // Will handle "age": null
)

// These JSON strings will decode successfully:
// {"name": "Alice", "age": 25}  -> User("Alice", Some(25))
// {"name": "Bob", "age": null}  -> User("Bob", None)
pub fn optional_field(
  named name: String,
  of inner_type: Decoder(t),
) -> FieldDecoder(option.Option(t))

Decode an absent or null field as None, and a present value with the inner decoder as Some. Use field with optional when the field must be present but may be null.

Example

type User {
  User(name: String, age: Option(Int))
}

let decoder = decode2(
  User,
  field("name", string()),
  optional_field("age", int())  // Will handle both missing "age" field and "age": null
)

// All these JSON strings will decode successfully:
// {"name": "Alice", "age": 25}     -> User("Alice", Some(25))
// {"name": "Bob", "age": null}     -> User("Bob", None)
// {"name": "Charlie"}              -> User("Charlie", None)
pub fn reuse_decoder(decoder: Decoder(t)) -> Decoder(t)

Put the decoder’s schema in $defs and return a decoder whose schema points to it with $ref. The name derives from the rendered schema’s SHA-1 hash; it is a deterministic definition key, not a collision guarantee.

Example

type Person {
  Person(name: String, friends: List(Pet))
}

type Pet {
  Pet(name: String)
}


let pet_decoder = reuse_decoder(
  decode2(
    Pet,
    field("name", string()),
  )
)

let person_decoder = reuse_decoder(
  decode2(
    Person,
    field("name", string()),
    field("friends", list(pet_decoder))
  )
)
pub fn self_decoder(lazy: fn() -> Decoder(t)) -> Decoder(t)

Decode a recursive type through a function that returns its decoder. The schema uses $ref: "#" for a root self-reference.

Wrap recursive decoders in reuse_decoder. Nested recursive types need their root references rebased to distinct definitions.

Example

// A binary tree type that can contain itself
pub type Tree {
  Node(value: Int, left: Option(Tree), right: Option(Tree))
  Leaf(value: Int)
}

// Create a recursive decoder for the Tree type
pub fn tree_decoder() -> Decoder(Tree) {
  // Use union_type_decoder for handling different variants
  union_type_decoder([
    #("leaf", decode1(Leaf, field("value", int()))),
    #("node", decode3(
      Node,
      field("value", int()),
      // Use self_decoder to handle recursive fields
      field("left", optional(self_decoder(tree_decoder))),
      field("right", optional(self_decoder(tree_decoder))),
    )),
  ])
  |> reuse_decoder
}
pub fn string() -> Decoder(String)
pub fn tuple2(
  first decode1: Decoder(a),
  second decode2: Decoder(b),
) -> Decoder(#(a, b))
pub fn tuple3(
  first decode1: Decoder(a),
  second decode2: Decoder(b),
  third decode3: Decoder(c),
) -> Decoder(#(a, b, c))
pub fn tuple4(
  first decode1: Decoder(a),
  second decode2: Decoder(b),
  third decode3: Decoder(c),
  fourth decode4: Decoder(d),
) -> Decoder(#(a, b, c, d))
pub fn tuple5(
  first decode1: Decoder(a),
  second decode2: Decoder(b),
  third decode3: Decoder(c),
  fourth decode4: Decoder(d),
  fifth decode5: Decoder(e),
) -> Decoder(#(a, b, c, d, e))
pub fn tuple6(
  first decode1: Decoder(a),
  second decode2: Decoder(b),
  third decode3: Decoder(c),
  fourth decode4: Decoder(d),
  fifth decode5: Decoder(e),
  sixth decode6: Decoder(f),
) -> Decoder(#(a, b, c, d, e, f))
pub fn union_type_decoder(
  constructor_decoders decoders: List(#(String, Decoder(a))),
) -> Decoder(a)

Decode a type/data union using the payload decoder for its label. Test every variant; the compiler cannot check that this list is exhaustive.

Example

type Shape {
  Circle(Float)
  Rectangle(Float, Float)
}

let shape_decoder = union_type_decoder([
  #("circle", decode1(Circle, field("radius", float()))),
  #("rectangle", decode2(Rectangle,
    field("width", float()),
    field("height", float())
  ))
])
pub fn union_type_encoder(
  value of: a,
  encoder_fn encoder_fn: fn(a) -> #(String, json.Json),
) -> json.Json

Encode a union as an object with type and data fields. The callback returns the variant label and its encoded payload. Keep the labels and payloads aligned with union_type_decoder.

Example

type Shape {
  Circle(Float)
  Rectangle(Float, Float)
}

let shape_encoder = union_type_encoder(fn(shape) {
  case shape {
    Circle(radius) -> #("circle", json.object([#("radius", json.float(radius))]))
    Rectangle(width, height) -> #(
      "rectangle",
      json.object([
        #("width", json.float(width)),
        #("height", json.float(height))
      ])
    )
  }
})
✨ Search Document