json/blueprint

Types

pub type Decoder(t) {
  Decoder(
    dyn_decoder: DynamicDecoder(t),
    schema: SchemaDefinition,
    defs: List(#(String, SchemaDefinition)),
  )
}

Constructors

  • Decoder(
      dyn_decoder: DynamicDecoder(t),
      schema: SchemaDefinition,
      defs: List(#(String, SchemaDefinition)),
    )
pub type FieldDecoder(t) {
  FieldDecoder(
    dyn_decoder: DynamicDecoder(t),
    field_schema: #(String, SchemaDefinition),
    defs: List(#(String, SchemaDefinition)),
  )
}

Constructors

  • FieldDecoder(
      dyn_decoder: DynamicDecoder(t),
      field_schema: #(String, SchemaDefinition),
      defs: List(#(String, SchemaDefinition)),
    )
pub type LazyDecoder(t) =
  fn() -> Decoder(t)

Functions

pub fn bool() -> Decoder(Bool)
pub fn decode(
  using decoder: Decoder(a),
  from json_string: String,
) -> Result(a, DecodeError)
pub fn decode0(constructor: a) -> Decoder(a)
pub fn decode1(
  constructor: fn(a) -> b,
  t1: FieldDecoder(a),
) -> Decoder(b)
pub fn decode2(
  constructor: fn(a, b) -> c,
  t1: FieldDecoder(a),
  t2: FieldDecoder(b),
) -> Decoder(c)
pub fn decode3(
  constructor: fn(a, b, c) -> d,
  t1: FieldDecoder(a),
  t2: FieldDecoder(b),
  t3: FieldDecoder(c),
) -> Decoder(d)
pub fn decode4(
  constructor: fn(a, b, c, d) -> e,
  t1: FieldDecoder(a),
  t2: FieldDecoder(b),
  t3: FieldDecoder(c),
  t4: FieldDecoder(d),
) -> Decoder(e)
pub fn decode5(
  constructor: fn(a, b, c, d, e) -> f,
  t1: FieldDecoder(a),
  t2: FieldDecoder(b),
  t3: FieldDecoder(c),
  t4: FieldDecoder(d),
  t5: FieldDecoder(e),
) -> Decoder(f)
pub fn decode6(
  constructor: fn(a, b, c, d, e, f) -> g,
  t1: FieldDecoder(a),
  t2: FieldDecoder(b),
  t3: FieldDecoder(c),
  t4: FieldDecoder(d),
  t5: FieldDecoder(e),
  t6: FieldDecoder(f),
) -> Decoder(g)
pub fn decode7(
  constructor: fn(a, b, c, d, e, f, g) -> h,
  t1: FieldDecoder(a),
  t2: FieldDecoder(b),
  t3: FieldDecoder(c),
  t4: FieldDecoder(d),
  t5: FieldDecoder(e),
  t6: FieldDecoder(f),
  t7: FieldDecoder(g),
) -> Decoder(h)
pub fn decode8(
  constructor: fn(a, b, c, d, e, f, g, h) -> i,
  t1: FieldDecoder(a),
  t2: FieldDecoder(b),
  t3: FieldDecoder(c),
  t4: FieldDecoder(d),
  t5: FieldDecoder(e),
  t6: FieldDecoder(f),
  t7: FieldDecoder(g),
  t8: FieldDecoder(h),
) -> Decoder(i)
pub fn decode9(
  constructor: fn(a, b, c, d, e, f, g, h, i) -> j,
  t1: FieldDecoder(a),
  t2: FieldDecoder(b),
  t3: FieldDecoder(c),
  t4: FieldDecoder(d),
  t5: FieldDecoder(e),
  t6: FieldDecoder(f),
  t7: FieldDecoder(g),
  t8: FieldDecoder(h),
  t9: FieldDecoder(i),
) -> Decoder(j)
pub fn encode_optional_field(
  fields fields: List(#(String, Json)),
  name name: String,
  maybe value: Option(a),
  encoder encode_fn: fn(a) -> Json,
) -> List(#(String, Json))

Adds an optional field to a list of key-value pairs that will be used to create a JSON object. This is particularly useful when defining JSON encoder to pair up to the optional_field decoder.

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,
  second encode2: fn(b) -> Json,
) -> Json
pub fn encode_tuple3(
  tuple tuple: #(a, b, c),
  first encode1: fn(a) -> Json,
  second encode2: fn(b) -> Json,
  third encode3: fn(c) -> Json,
) -> Json
pub fn encode_tuple4(
  tuple tuple: #(a, b, c, d),
  first encode1: fn(a) -> Json,
  second encode2: fn(b) -> Json,
  third encode3: fn(c) -> Json,
  fourth encode4: fn(d) -> Json,
) -> Json
pub fn encode_tuple5(
  tuple tuple: #(a, b, c, d, e),
  first encode1: fn(a) -> Json,
  second encode2: fn(b) -> Json,
  third encode3: fn(c) -> Json,
  fourth encode4: fn(d) -> Json,
  fifth encode5: fn(e) -> Json,
) -> Json
pub fn encode_tuple6(
  tuple tuple: #(a, b, c, d, e, f),
  first encode1: fn(a) -> Json,
  second encode2: fn(b) -> Json,
  third encode3: fn(c) -> Json,
  fourth encode4: fn(d) -> Json,
  fifth encode5: fn(e) -> Json,
  sixth encode6: fn(f) -> Json,
) -> Json
pub fn encode_tuple7(
  tuple tuple: #(a, b, c, d, e, f, g),
  first encode1: fn(a) -> Json,
  second encode2: fn(b) -> Json,
  third encode3: fn(c) -> Json,
  fourth encode4: fn(d) -> Json,
  fifth encode5: fn(e) -> Json,
  sixth encode6: fn(f) -> Json,
  seventh encode7: fn(g) -> Json,
) -> Json
pub fn encode_tuple8(
  tuple tuple: #(a, b, c, d, e, f, g, h),
  first encode1: fn(a) -> Json,
  second encode2: fn(b) -> Json,
  third encode3: fn(c) -> Json,
  fourth encode4: fn(d) -> Json,
  fifth encode5: fn(e) -> Json,
  sixth encode6: fn(f) -> Json,
  seventh encode7: fn(g) -> Json,
  eighth encode8: fn(h) -> Json,
) -> Json
pub fn encode_tuple9(
  tuple tuple: #(a, b, c, d, e, f, g, h, i),
  first encode1: fn(a) -> Json,
  second encode2: fn(b) -> Json,
  third encode3: fn(c) -> Json,
  fourth encode4: fn(d) -> Json,
  fifth encode5: fn(e) -> Json,
  sixth encode6: fn(f) -> Json,
  seventh encode7: fn(g) -> Json,
  eighth encode8: fn(h) -> Json,
  ninth encode9: fn(i) -> Json,
) -> Json
pub fn enum_type_decoder(
  constructor_decoders decoders: List(#(String, a)),
) -> Decoder(a)

Function to define a decoder for enum types (unions where constructors have no arguments). The function takes a list of tuples containing the string representation and the corresponding enum value.

Make sure to add tests for every possible enum value because it is not possible to check for exhaustiveness.

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

Function to encode an enum type (unions where constructors have no arguments) into a JSON object. The function takes a value and an encoder function that returns the string representation of the enum value.

Make sure to update the decoder function accordingly.

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(a),
) -> FieldDecoder(a)
pub fn float() -> Decoder(Float)
pub fn generate_json_schema(decoder: Decoder(a)) -> Json
pub fn get_dynamic_decoder(
  decoder: Decoder(a),
) -> fn(Dynamic) -> Result(a, List(DecodeError))
pub fn int() -> Decoder(Int)
pub fn list(of decoder_type: Decoder(a)) -> Decoder(List(a))
pub fn map(
  decoder decoder: Decoder(a),
  over foo: fn(a) -> b,
) -> Decoder(b)
pub fn optional(of decode: Decoder(a)) -> Decoder(Option(a))

Creates a decoder that can handle null values by wrapping the result in an Option type. When the value is null, it returns None. Otherwise, it uses the provided decoder to decode the value and wraps the result in Some. If you need the decoder to handle a possible missing field (i.e., the field is absent from the JSON), use the optional_field function instead.

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(a),
) -> FieldDecoder(Option(a))

Decode a field that can be missing or have a null value into an Option type. This function is useful when you want to handle both cases where a field is absent from the JSON or when it’s explicitly set to null.

If you only need to handle fields that are present but might be null, use the optional function instead.

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(a)) -> Decoder(a)

Creates a reusable version of a decoder that can be used multiple times in a schema without duplicating the schema definition.

The function:

  1. Creates a unique reference name based on the schema’s hash
  2. Moves the original schema into the $defs section
  3. Returns a new decoder that references the schema via $ref

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(a)) -> Decoder(a)

Creates a decoder for recursive data types by allowing self-referential definitions. This is useful when you have types that contain themselves, like trees or linked lists.

The function takes a lazy decoder (a function that returns a decoder) to break the recursive dependency cycle. The returned decoder uses a JSON Schema reference “#” to point to the root schema definition.

IMPORTANT Add the reuse_decoder when there are nested recursive types so the schema references (#) get rewritten correctly and self-references from the different types don’t get mixed up. As a recommendation, always add it when decoding recursive types.

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)

Function to defined a decoder for a union types. The function takes a list of decoders for each possible type of the union.

Make sure to add tests for every possible type of the union because it is not possible to check for exhaustiveness in the case.

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

Function to encode a union type into a JSON object. The function takes a value and an encoder function that returns a tuple of the type name and the JSON value.

Make sure to update the decoder function accordingly.

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