sextant

Sextant - A Gleam library for JSON Schema generation and validation.

This library provides a use-based API for defining JSON schemas that can both generate JSON Schema 2020-12 documents and validate dynamic data.

Example

import sextant
import gleam/option.{type Option}

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

fn user_schema() -> sextant.JsonSchema(User) {
  use name <- sextant.field("name", sextant.string() |> sextant.min_length(1))
  use age <- sextant.field("age", sextant.integer() |> sextant.int_min(0))
  use email <- sextant.optional_field("email", sextant.string())
  sextant.success(User(name:, age:, email:))
}

// Generate JSON Schema
let schema_json = sextant.to_json(user_schema())

// Validate data
let result = sextant.run(dynamic_data, user_schema())

Types

Array-specific constraint violations.

pub type ArrayConstraintViolation {
  ArrayTooShort(min: Int, actual: Int)
  ArrayTooLong(max: Int, actual: Int)
  ItemsNotUnique
}

Constructors

  • ArrayTooShort(min: Int, actual: Int)
  • ArrayTooLong(max: Int, actual: Int)
  • ItemsNotUnique

Union type for all constraint violations.

pub type ConstraintViolation {
  StringViolation(StringConstraintViolation)
  NumberViolation(NumberConstraintViolation)
  ArrayViolation(ArrayConstraintViolation)
  CustomViolation(message: String)
}

Constructors

A JSON Schema that can generate schema documents and validate data.

The type parameter a is the Gleam type that this schema decodes to.

pub opaque type JsonSchema(a)

Number-specific constraint violations (applies to both Int and Float).

pub type NumberConstraintViolation {
  NumberTooSmall(minimum: Float, exclusive: Bool, actual: Float)
  NumberTooLarge(maximum: Float, exclusive: Bool, actual: Float)
  NotMultipleOf(multiple: Float, actual: Float)
}

Constructors

  • NumberTooSmall(minimum: Float, exclusive: Bool, actual: Float)
  • NumberTooLarge(maximum: Float, exclusive: Bool, actual: Float)
  • NotMultipleOf(multiple: Float, actual: Float)

Options for controlling schema validation behaviour.

Use these options with run_with_options to customize validation. The default options are available as default_options.

Example

// Enable format validation (disabled by default per JSON Schema spec)
let opts = sextant.Options(validate_formats: True)
sextant.run_with_options(data, email_schema, opts)
pub type Options {
  Options(validate_formats: Bool)
}

Constructors

  • Options(validate_formats: Bool)

    Arguments

    validate_formats

    Whether to validate string formats (email, uri, etc.). Disabled by default as per JSON Schema specification, which treats formats as annotations rather than assertions.

String-specific constraint violations.

pub type StringConstraintViolation {
  StringTooShort(min: Int, actual: Int)
  StringTooLong(max: Int, actual: Int)
  PatternMismatch(pattern: String, actual: String)
  InvalidPattern(pattern: String, error: String)
  InvalidFormat(format: String, actual: String)
}

Constructors

  • StringTooShort(min: Int, actual: Int)
  • StringTooLong(max: Int, actual: Int)
  • PatternMismatch(pattern: String, actual: String)
  • InvalidPattern(pattern: String, error: String)
  • InvalidFormat(format: String, actual: String)

String format types as defined in JSON Schema.

pub type StringFormat {
  Email
  Uri
  DateTime
  Date
  Time
  Uuid
  Hostname
  Ipv4
  Ipv6
}

Constructors

  • Email
  • Uri
  • DateTime
  • Date
  • Time
  • Uuid
  • Hostname
  • Ipv4
  • Ipv6

Top-level validation error returned when schema validation fails.

pub type ValidationError {
  TypeError(expected: String, found: String, path: List(String))
  ConstraintError(
    violation: ConstraintViolation,
    path: List(String),
  )
  MissingField(field: String, path: List(String))
  UnknownVariant(
    value: String,
    expected: List(String),
    path: List(String),
  )
  ConstMismatch(
    expected: String,
    actual: String,
    path: List(String),
  )
}

Constructors

  • TypeError(expected: String, found: String, path: List(String))
  • ConstraintError(
      violation: ConstraintViolation,
      path: List(String),
    )
  • MissingField(field: String, path: List(String))
  • UnknownVariant(
      value: String,
      expected: List(String),
      path: List(String),
    )
  • ConstMismatch(
      expected: String,
      actual: String,
      path: List(String),
    )

Values

pub fn additional_properties(
  schema: JsonSchema(a),
  allow: Bool,
) -> JsonSchema(a)

Allow additional properties in the generated JSON Schema.

By default, object schemas set additionalProperties: false to enforce strict validation. Use this combinator to allow extra properties that aren’t defined in the schema.

Note: This only affects the generated JSON Schema document. Sextant’s decoder always ignores additional properties during validation.

Example

fn user_schema() -> sextant.JsonSchema(User) {
  use name <- sextant.field("name", sextant.string())
  sextant.success(User(name:))
}
|> sextant.additional_properties(True)
pub fn any_of(
  first: JsonSchema(a),
  rest: List(JsonSchema(a)),
) -> JsonSchema(a)

Create a schema where at least one of the variants must match.

Note: At runtime, one_of and any_of have identical validation behaviour - the first matching schema is used. The distinction only affects the generated JSON Schema output (oneOf vs anyOf).

Example

let flexible_schema = sextant.any_of(
  sextant.string() |> sextant.map(process_string),
  [sextant.integer() |> sextant.map(process_int)],
)
pub fn array(of inner: JsonSchema(a)) -> JsonSchema(List(a))

Create a schema for JSON arrays.

Example

let tags_schema = sextant.array(of: sextant.string())
pub fn boolean() -> JsonSchema(Bool)

Create a schema for JSON booleans.

Example

let active_schema = sextant.boolean()
pub fn const_value(
  schema: JsonSchema(a),
  value: a,
  to_json: fn(a) -> json.Json,
) -> JsonSchema(a)

Constrain a schema to accept only a specific constant value.

The to_json function converts the value to its JSON representation for schema generation. For primitives, use the corresponding json function.

Example

let version_schema = sextant.string() |> sextant.const_value("v1", json.string)
let answer_schema = sextant.integer() |> sextant.const_value(42, json.int)
let enabled_schema = sextant.boolean() |> sextant.const_value(True, json.bool)
pub fn default(
  schema: JsonSchema(a),
  def: json.Json,
) -> JsonSchema(a)

Add a default value to the schema.

Example

let count_schema = sextant.integer()
  |> sextant.default(json.int(0))
pub const default_options: Options

Default validation options.

  • validate_formats: False (formats are treated as annotations only)
pub fn describe(
  schema: JsonSchema(a),
  description: String,
) -> JsonSchema(a)

Add a description to the schema.

Example

let name_schema = sextant.string() |> sextant.describe("The user's name")
pub fn dict(
  value_schema: JsonSchema(v),
) -> JsonSchema(dict.Dict(String, v))

Create a schema for JSON objects with arbitrary string keys.

Example

let scores_schema = sextant.dict(sextant.integer())
// Validates: {"alice": 100, "bob": 85}
pub fn enum(
  first: #(String, a),
  rest: List(#(String, a)),
) -> JsonSchema(a)

Create a schema for a string enum that maps to Gleam values.

Example

type Role {
  Admin
  Member
  Guest
}

let role_schema = sextant.enum(#("admin", Admin), [
  #("member", Member),
  #("guest", Guest),
])
pub fn error_to_string(error: ValidationError) -> String

Convert a validation error to a human-readable string.

pub fn examples(
  schema: JsonSchema(a),
  ex: List(json.Json),
) -> JsonSchema(a)

Add examples to the schema.

Example

let name_schema = sextant.string()
  |> sextant.examples([json.string("Alice"), json.string("Bob")])
pub fn field(
  name: String,
  field_schema: JsonSchema(a),
  next: fn(a) -> JsonSchema(b),
) -> JsonSchema(b)

Extract a required field from an object.

The field must be present in the JSON object, otherwise a MissingField error is returned.

Example

use name <- sextant.field("name", sextant.string())
use age <- sextant.field("age", sextant.integer())
sextant.success(User(name:, age:))
pub fn float_exclusive_max(
  schema: JsonSchema(Float),
  max: Float,
) -> JsonSchema(Float)

Set exclusive maximum value for floats.

Example

let under_100 = sextant.number() |> sextant.float_exclusive_max(100.0)
pub fn float_exclusive_min(
  schema: JsonSchema(Float),
  min: Float,
) -> JsonSchema(Float)

Set exclusive minimum value for floats.

Example

let positive_schema = sextant.number() |> sextant.float_exclusive_min(0.0)
pub fn float_max(
  schema: JsonSchema(Float),
  max: Float,
) -> JsonSchema(Float)

Set inclusive maximum value for floats.

Example

let percentage_schema = sextant.number() |> sextant.float_max(100.0)
pub fn float_min(
  schema: JsonSchema(Float),
  min: Float,
) -> JsonSchema(Float)

Set inclusive minimum value for floats.

Example

let price_schema = sextant.number() |> sextant.float_min(0.0)
pub fn float_multiple_of(
  schema: JsonSchema(Float),
  multiple: Float,
) -> JsonSchema(Float)

Set that the float must be a multiple of a given value.

Note: Due to floating-point precision, a small tolerance (1e-7) is used when checking the remainder. If multiple is 0.0, all values will be considered valid since x %.. 0.0 = 0.0 in Gleam.

Example

let quarter_schema = sextant.number() |> sextant.float_multiple_of(0.25)
pub fn format(
  schema: JsonSchema(String),
  fmt: StringFormat,
) -> JsonSchema(String)

Set a string format constraint.

Format validation only runs when Options.validate_formats is True.

Example

let email_schema = sextant.string() |> sextant.format(sextant.Email)
pub fn int_exclusive_max(
  schema: JsonSchema(Int),
  max: Int,
) -> JsonSchema(Int)

Set exclusive maximum value for integers.

Example

let under_100 = sextant.integer() |> sextant.int_exclusive_max(100)
pub fn int_exclusive_min(
  schema: JsonSchema(Int),
  min: Int,
) -> JsonSchema(Int)

Set exclusive minimum value for integers.

Example

let positive_schema = sextant.integer() |> sextant.int_exclusive_min(0)
pub fn int_max(
  schema: JsonSchema(Int),
  max: Int,
) -> JsonSchema(Int)

Set inclusive maximum value for integers.

Example

let age_schema = sextant.integer() |> sextant.int_max(150)
pub fn int_min(
  schema: JsonSchema(Int),
  min: Int,
) -> JsonSchema(Int)

Set inclusive minimum value for integers.

Example

let age_schema = sextant.integer() |> sextant.int_min(0)
pub fn int_multiple_of(
  schema: JsonSchema(Int),
  multiple: Int,
) -> JsonSchema(Int)

Set that the integer must be a multiple of a given value.

Note: If multiple is 0, all values will be considered valid since x % 0 = 0 in Gleam.

Example

let even_schema = sextant.integer() |> sextant.int_multiple_of(2)
pub fn integer() -> JsonSchema(Int)

Create a schema for JSON integers.

Example

let age_schema = sextant.integer()
pub fn map(
  schema: JsonSchema(a),
  transform: fn(a) -> b,
) -> JsonSchema(b)

Transform the decoded value using a function.

Example

let uppercase_string = sextant.string() |> sextant.map(string.uppercase)
pub fn max_items(
  schema: JsonSchema(List(a)),
  max: Int,
) -> JsonSchema(List(a))

Set maximum number of items in an array.

Example

let tags_schema = sextant.array(sextant.string()) |> sextant.max_items(10)
pub fn max_length(
  schema: JsonSchema(String),
  max: Int,
) -> JsonSchema(String)

Set maximum string length.

Example

let name_schema = sextant.string() |> sextant.max_length(100)
pub fn min_items(
  schema: JsonSchema(List(a)),
  min: Int,
) -> JsonSchema(List(a))

Set minimum number of items in an array.

Example

let tags_schema = sextant.array(sextant.string()) |> sextant.min_items(1)
pub fn min_length(
  schema: JsonSchema(String),
  min: Int,
) -> JsonSchema(String)

Set minimum string length.

Example

let name_schema = sextant.string() |> sextant.min_length(1)
pub fn null() -> JsonSchema(Nil)

Create a schema for JSON null.

Example

let null_schema = sextant.null()
pub fn number() -> JsonSchema(Float)

Create a schema for JSON numbers (floats).

Example

let price_schema = sextant.number()
pub fn one_of(
  first: JsonSchema(a),
  rest: List(JsonSchema(a)),
) -> JsonSchema(a)

Create a schema where exactly one of the variants must match.

Note: At runtime, one_of and any_of have identical validation behaviour - the first matching schema is used. The distinction only affects the generated JSON Schema output (oneOf vs anyOf).

Example

let string_or_int = sextant.one_of(
  sextant.string() |> sextant.map(StringValue),
  [sextant.integer() |> sextant.map(IntValue)],
)
pub fn optional(
  inner: JsonSchema(a),
) -> JsonSchema(option.Option(a))

Create a schema that allows null values.

Returns Some(value) if the value is present and valid, None if null.

Example

let optional_name = sextant.optional(sextant.string())
pub fn optional_field(
  name: String,
  field_schema: JsonSchema(a),
  next: fn(option.Option(a)) -> JsonSchema(b),
) -> JsonSchema(b)

Extract an optional field from an object.

Returns Some(value) if the field is present, None if missing.

Example

use name <- sextant.field("name", sextant.string())
use email <- sextant.optional_field("email", sextant.string())
sextant.success(User(name:, email:))
pub fn pattern(
  schema: JsonSchema(String),
  pattern_string: String,
) -> JsonSchema(String)

Set a regex pattern the string must match.

If the pattern is not a valid regex, validation will return an InvalidPattern constraint error.

Example

let slug_schema = sextant.string() |> sextant.pattern("^[a-z0-9-]+$")
pub fn run(
  data: dynamic.Dynamic,
  schema: JsonSchema(a),
) -> Result(a, List(ValidationError))

Validate and decode dynamic data against a schema.

Example

case sextant.run(data, user_schema()) {
  Ok(user) -> // use the decoded user
  Error(errors) -> // handle validation errors
}
pub fn run_with_options(
  data: dynamic.Dynamic,
  schema: JsonSchema(a),
  options: Options,
) -> Result(a, List(ValidationError))

Validate and decode dynamic data with custom options.

Example

let opts = sextant.Options(validate_formats: True)
case sextant.run_with_options(data, email_schema, opts) {
  Ok(email) -> // email format was validated
  Error(errors) -> // handle validation errors
}
pub fn string() -> JsonSchema(String)

Create a schema for JSON strings.

Example

let name_schema = sextant.string()
pub fn success(value: a) -> JsonSchema(a)

Finalise a schema with a successfully constructed value.

This is the terminal function in a use-chain for object schemas.

Example

use name <- sextant.field("name", sextant.string())
sextant.success(User(name:))
pub fn timestamp() -> JsonSchema(timestamp.Timestamp)

Create a schema for RFC 3339 timestamps that decodes to gleam/time/timestamp.Timestamp.

This validates and parses the string as an RFC 3339 datetime, returning the proper Timestamp type from gleam_time. Use this instead of string() |> format(DateTime) when you want a typed timestamp value.

Example

use created_at <- sextant.field("created_at", sextant.timestamp())
sextant.success(Event(created_at:, ...))
pub fn title(
  schema: JsonSchema(a),
  title_text: String,
) -> JsonSchema(a)

Add a title to the schema.

Example

let name_schema = sextant.string() |> sextant.title("User Name")
pub fn to_json(schema: JsonSchema(a)) -> json.Json

Generate a JSON Schema 2020-12 document.

Example

let schema_json = sextant.to_json(user_schema())
pub fn try_map(
  schema: JsonSchema(a),
  transform: fn(a) -> Result(b, String),
  default default: b,
) -> JsonSchema(b)

Transform the decoded value using a fallible function.

If the transform returns Error, it’s converted to a ConstraintError using the provided error message. The default value is used as the zero value for schema extraction and as the fallback when the transform fails.

Use this for validations that can’t be expressed with built-in constraints, such as parsing strings into custom types or cross-field validation.

Example

pub type Slug { Slug(String) }

fn parse_slug(s: String) -> Result(Slug, String) {
  let is_valid = string.length(s) > 0
    && string.lowercase(s) == s
    && !string.contains(s, " ")
  case is_valid {
    True -> Ok(Slug(s))
    False -> Error("must be lowercase with no spaces")
  }
}

let slug_schema = sextant.string()
  |> sextant.try_map(parse_slug, default: Slug("default"))
pub fn tuple2(
  first: JsonSchema(a),
  second: JsonSchema(b),
) -> JsonSchema(#(a, b))

Create a schema for a fixed-length array with 2 elements of different types.

This generates a JSON Schema with prefixItems and exact length constraints.

Example

// A point as [x, y] coordinates
let point_schema = sextant.tuple2(sextant.number(), sextant.number())

// A key-value pair as [string, int]
let pair_schema = sextant.tuple2(sextant.string(), sextant.integer())
pub fn tuple3(
  first: JsonSchema(a),
  second: JsonSchema(b),
  third: JsonSchema(c),
) -> JsonSchema(#(a, b, c))

Create a schema for a fixed-length array with 3 elements of different types.

This generates a JSON Schema with prefixItems and exact length constraints.

Example

// RGB colour as [r, g, b]
let rgb_schema = sextant.tuple3(
  sextant.integer() |> sextant.int_min(0) |> sextant.int_max(255),
  sextant.integer() |> sextant.int_min(0) |> sextant.int_max(255),
  sextant.integer() |> sextant.int_min(0) |> sextant.int_max(255),
)
pub fn tuple4(
  first: JsonSchema(a),
  second: JsonSchema(b),
  third: JsonSchema(c),
  fourth: JsonSchema(d),
) -> JsonSchema(#(a, b, c, d))

Create a schema for a fixed-length array with 4 elements of different types.

This generates a JSON Schema with prefixItems and exact length constraints.

Example

// RGBA colour as [r, g, b, a]
let rgba_schema = sextant.tuple4(
  sextant.integer(),
  sextant.integer(),
  sextant.integer(),
  sextant.number(),
)
pub fn tuple5(
  first: JsonSchema(a),
  second: JsonSchema(b),
  third: JsonSchema(c),
  fourth: JsonSchema(d),
  fifth: JsonSchema(e),
) -> JsonSchema(#(a, b, c, d, e))

Create a schema for a fixed-length array with 5 elements of different types.

This generates a JSON Schema with prefixItems and exact length constraints.

pub fn tuple6(
  first: JsonSchema(a),
  second: JsonSchema(b),
  third: JsonSchema(c),
  fourth: JsonSchema(d),
  fifth: JsonSchema(e),
  sixth: JsonSchema(f),
) -> JsonSchema(#(a, b, c, d, e, f))

Create a schema for a fixed-length array with 6 elements of different types.

This generates a JSON Schema with prefixItems and exact length constraints.

pub fn unique_items(
  schema: JsonSchema(List(a)),
) -> JsonSchema(List(a))

Require all array items to be unique.

Example

let unique_tags = sextant.array(sextant.string()) |> sextant.unique_items()
pub fn uri() -> JsonSchema(uri.Uri)

Create a schema for URIs that decodes to gleam/uri.Uri.

This validates and parses the string as a URI, returning the proper Uri type from gleam_stdlib. Use this instead of string() |> format(Uri) when you want a typed URI value.

Example

use website <- sextant.optional_field("website", sextant.uri())
sextant.success(Profile(website:, ...))
pub fn uuid() -> JsonSchema(uuid.Uuid)

Create a schema for UUIDs that decodes to youid/uuid.Uuid.

This validates and parses the string as a UUID, returning the proper Uuid type from the youid library. Use this instead of string() |> format(Uuid) when you want a typed UUID value.

Example

use id <- sextant.field("id", sextant.uuid())
sextant.success(User(id:, ...))
Search Document