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 FieldDecoder(t)
pub type LazyDecoder(t) =
fn() -> Decoder(t)
Values
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 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 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 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))
])
)
}
})