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
-
StringViolation(StringConstraintViolation) -
NumberViolation(NumberConstraintViolation) -
ArrayViolation(ArrayConstraintViolation) -
CustomViolation(message: String)Custom validation error from
try_mapor other user-defined validations.
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:, ...))