howdy/openapi/schema

Schemas describe a JSON value once and give you everything built from that description: the decoder a handler reads it with, the encoder a response writes it with, and the JSON Schema the OpenAPI document shows. Because all three come from one definition, the document cannot drift from what the server really accepts and sends.

import howdy/openapi/schema.{type Schema}

pub fn user() -> Schema(User) {
  {
    use id <- schema.field("id", schema.int(), fn(user: User) { user.id })
    use name <- schema.field(
      "name",
      schema.string() |> schema.max_length(50),
      fn(user: User) { user.name },
    )
    schema.success(User(id:, name:))
  }
  |> schema.named("User")
}

Objects are built like gleam/dynamic/decode decoders, with one extra argument per field: a function reading the field back out of the value, which is what lets the same schema encode.

To document an object, the fields after each one are found by calling the rest of the chain with a placeholder value, such as "" or 0. So keep the chain to field calls and a final success: code that branches on a field’s value only has its placeholder branch documented.

Constraints such as max_length are checked while decoding, and shown in the document as the matching JSON Schema keyword. rule runs any howdy/validate rule too, but the document cannot show what it checks.

Types

Named schemas seen while rendering, as their JSON and its text, so a second, different schema with the same name can be caught.

pub opaque type Components

The JSON Schema of a schema, without its types. What howdy/openapi renders into the document.

A named node is shown as a reference to its component, with outer keywords, those added after naming, beside the reference. kind is the JSON type, rendered as the type keyword; a node that is a choice between others, such as nullable or one_of, has none.

pub opaque type Node

A JSON value’s description, decoder and encoder, for values of type a.

pub opaque type Schema(a)

One shape a one_of accepts: inner describes and decodes it, and to makes the union’s value from the decoded one.

pub opaque type Variant(a)

Values

pub fn bool() -> Schema(Bool)

true or false. As a parameter, only the text true or false.

pub fn decoder(schema: Schema(a)) -> decode.Decoder(a)

The decoder for a schema, for reading JSON outside a handler.

pub fn deprecated(schema: Schema(a)) -> Schema(a)
pub fn description(schema: Schema(a), text: String) -> Schema(a)
pub fn dict(of inner: Schema(a)) -> Schema(dict.Dict(String, a))

A JSON object used as a map from string keys to values of one schema.

pub fn email(schema: Schema(String)) -> Schema(String)

The light email check from howdy/validate, shown as format: email.

pub fn enum(
  variants: List(a),
  name: fn(a) -> String,
) -> Schema(a)

A string that is the name of one of variants, as name gives it. Decoding accepts only the listed variants; encoding names any value with name, so write it as a case over the type and the compiler sees that every variant has a name. Panics if variants is empty.

schema.enum([Admin, Member], fn(role) {
  case role {
    Admin -> "admin"
    Member -> "member"
  }
})
pub fn example(schema: Schema(a), value: a) -> Schema(a)

An example value, encoded with the schema itself.

pub fn field(
  name: String,
  schema: Schema(a),
  get: fn(r) -> a,
  next: fn(a) -> Schema(r),
) -> Schema(r)

A required object field. get reads the field from the finished value, for encoding. See the module docs for how chains are built.

pub fn float() -> Schema(Float)

A JSON number. Integers such as 5 are accepted too.

pub fn float_maximum(
  schema: Schema(Float),
  value: Float,
) -> Schema(Float)

maximum for a float, with the message "must be at most 9.5".

pub fn float_minimum(
  schema: Schema(Float),
  value: Float,
) -> Schema(Float)

minimum for a float, with the message "must be at least 0.5".

pub fn format(schema: Schema(a), name: String) -> Schema(a)

A format hint such as "date-time" or "uuid". Only documents the value; pair it with try_map or rule to check it.

pub fn int() -> Schema(Int)
pub fn list(of inner: Schema(a)) -> Schema(List(a))

A JSON array. As a query parameter, every occurrence of the key, as in ?tag=a&tag=b.

pub fn map(
  schema: Schema(a),
  to to: fn(a) -> b,
  from from: fn(b) -> a,
) -> Schema(b)

Change the type a schema decodes to and encodes from. Both directions must always succeed; see try_map for conversions that can fail.

pub fn user_id() -> Schema(UserId) {
  schema.int() |> schema.map(to: UserId, from: fn(id) { id.value })
}
pub fn max_items(
  schema: Schema(List(a)),
  count: Int,
) -> Schema(List(a))

At most count items, shown as maxItems, with the message "must have at most 10 items".

pub fn max_length(
  schema: Schema(String),
  length: Int,
) -> Schema(String)
pub fn maximum(schema: Schema(Int), value: Int) -> Schema(Int)
pub fn message(error: decode.DecodeError) -> String

The English message for a decode error from a schema.

pub fn min_items(
  schema: Schema(List(a)),
  count: Int,
) -> Schema(List(a))

At least count items, shown as minItems, with the message "must have at least 1 item".

pub fn min_length(
  schema: Schema(String),
  length: Int,
) -> Schema(String)
pub fn minimum(schema: Schema(Int), value: Int) -> Schema(Int)
pub fn named(schema: Schema(a), name: String) -> Schema(a)

Show the schema once, under components/schemas/<name>, and refer to it by name everywhere it is used. Two different schemas with the same name make building the document panic.

pub fn not_empty(schema: Schema(String)) -> Schema(String)

At least one character. Shown as minLength: 1, with the message "must not be empty".

pub fn nullable(inner: Schema(a)) -> Schema(option.Option(a))

A value that may be null. For a field that may be left out, use optional_field instead.

pub fn one_of(
  variants: List(Variant(a)),
  encode encode: fn(a) -> json.Json,
) -> Schema(a)

One of several shapes, each described by its own schema and turned into a variant of one type with variant. Decoding tries the variants in order and takes the first that matches, so put the most specific first. Encoding is encode, which picks the variant’s schema for a value: write it as a case over the type, with schema.to_json, and the compiler sees that every variant is covered. Shown as anyOf over the variants’ schemas. Panics if variants is empty.

schema.one_of(
  [
    schema.variant(circle(), fn(c) { Circle(c) }),
    schema.variant(square(), fn(s) { Square(s) }),
  ],
  encode: fn(shape) {
    case shape {
      Circle(c) -> schema.to_json(c, circle())
      Square(s) -> schema.to_json(s, square())
    }
  },
)
pub fn optional_field(
  name: String,
  schema: Schema(a),
  get: fn(r) -> option.Option(a),
  next: fn(option.Option(a)) -> Schema(r),
) -> Schema(r)

An object field that may be missing or null, both of which decode to None. None is left out when encoding.

pub fn problem(error: decode.DecodeError) -> problem.Problem

The problem a decode error from a schema describes. A rule’s problem is read back as it was; the decoders’ type names become the matching problem; any other text, such as a try_map message, is a Custom problem with the code invalid.

pub fn rule(
  schema: Schema(a),
  rule: fn(a) -> Result(a, problem.Problem),
) -> Schema(a)

Run a howdy/validate rule while decoding. Rules may change the value, as validate.trim() does, and a failing rule reports its problem for the field. The document does not show what a rule checks; add a description if clients need to know.

pub fn string() -> Schema(String)
pub fn success(value: r) -> Schema(r)

Finish an object with the value built from its fields.

pub fn to_json(value: a, schema: Schema(a)) -> json.Json

Encode a value, for a response body. Pass it to service.respond as schema.to_json(_, user()).

pub fn try_map(
  schema: Schema(a),
  to to: fn(a) -> Result(b, String),
  from from: fn(b) -> a,
  placeholder placeholder: b,
) -> Schema(b)

Like map, but decoding can fail with a message such as "must be a date". placeholder stands in for the value when the document is built and when decoding fails.

pub fn variant(inner: Schema(b), to to: fn(b) -> a) -> Variant(a)
✨ Search Document