Advanced definitions and documentation
These examples extend the core record and array workflows. Acceptance and output semantics are specified in the technical contract.
To bind a component handler to its input definition, see Verified JSON boundaries.
Schema annotations
examples(definition, values) adds the standard JSON Schema examples
annotation. Values must be admitted Json; they are not required to satisfy
that definition. This metadata changes neither acceptance nor decoding and
runs no conversion callbacks. Repeated calls replace the examples at that node.
The HTML renderer displays these samples as metadata. For examples checked against
an input contract before rendering, use documentation.render_examples instead.
default(definition, value) adds a standalone default annotation. It neither
checks the sample against the definition nor substitutes it during decoding.
Use defaulted for a record field whose absence should supply a validated value;
that field’s actual default overrides any standalone annotation on its child.
format(definition, "email") describes a format without validating it. Standard
and custom names are accepted; a non-email string still passes a text definition
annotated as email. HTML documentation labels this metadata as an annotation.
Schema consumers must leave format assertions disabled to use the same acceptance
semantics. Asserted formats are not supplied by this operation.
assert_format(definition, Uuid) additionally checks the 36-character UUID
hexadecimal/dash syntax, preserving the original case and decoded output. Nil
UUIDs are accepted; URN prefixes, whitespace and missing dashes are rejected.
It emits explicit length and pattern constraints, so schema consumers enforce
the same checks without enabling optional format assertions. It does not restrict
UUID version or variant bits. As in JSON Schema, format constraints apply only
to string inputs; the base definition governs other types. UUID is the only
asserted format currently supplied. Email, URI and date/time remain annotation-only.
Supply primitive bounds and patterns before assert_format, which composes a
conditional definition rather than a direct string node.
Property dependencies
dependent_required(definition, "token", ["expires_at"]) constrains a record or
dictionary: when the original input contains token, it must also contain
expires_at. The dependency is one-way. Null counts as present, and defaults
inserted during decoding do not trigger a dependency. Apply it before wrapping
an object definition with nullable or a union. Repeated rules for a trigger
merge their required keys, removing duplicates; empty lists and self-dependencies
are allowed. Schema emission uses dependentRequired, and the HTML renderer
lists the presence rules beneath the object heading.
Fractional numbers
number() returns Definition(Float) and accepts every finite number admitted by
parse, including integers and magnitudes outside the safe-integer range.
number_bounds intersects finite numeric endpoints; None leaves that side
unbounded. Apply bounds before naming a definition or composing it into a union.
Annotations and maps preserve the underlying number node and support bounds.
import gargamelle
import gleam/option.{None, Some}
let assert Ok(probability) =
gargamelle.number_bounds(
gargamelle.number(),
Some(gargamelle.Inclusive(0.0)),
Some(gargamelle.Inclusive(1.0)),
)
let assert Ok(positive) =
gargamelle.number_bounds(gargamelle.number(), Some(gargamelle.Exclusive(0.0)), None)
This is the parsed IEEE-754 binary64 model, not arbitrary-precision decimal
arithmetic. parse rejects numeric overflow to infinity as an admission error,
including overflow nested in arrays/objects. Parsing may round decimal text or
underflow it to zero. Decoding preserves the resulting number, including negative
zero; schema comparisons treat both zero signs equally. Serializing through
to_string follows JSON serialization and normalizes negative zero to 0.
Bounds emit minimum/maximum or exclusiveMinimum/exclusiveMaximum.
Repeated bounds intersect, with exclusivity winning at equal endpoints.
Nonfinite endpoints, inverted intervals, and exclusive equal endpoints are
construction errors. An interval between adjacent representable numbers can be
constructed even when it accepts no binary64 value; schema and decoder agree.
bounds remains the integer/size API; use number_bounds for numbers.
General fractional multipleOf and fractional-number constants are not supported.
integer_multiple_of(safe_integer(), 3) adds divisibility by a positive safe
integer. It uses exact integer remainders, rejects zero/negative/unsafe divisors,
and emits multipleOf. It supports bounded or mapped safe-integer definitions;
apply it before naming. Repeated divisors intersect without computing an
overflow-prone least common multiple. It does not accept number() definitions.
Recursive definitions
recursive(name, build) creates a guarded self-recursive definition. Its builder
runs once with a typed construction plan, never a fabricated decoded value.
The builder returns a plan using the same recursion environment; recursive
returns a completed Definition(a). Ordinary examples infer these types:
import gargamelle as s
import gleam/result
pub type Tree {
Leaf(Float)
Branch(List(Tree))
}
pub fn tree_definition() {
s.recursive("tree", fn(self) {
use leaf <- result.try(s.required(s.record(Leaf), "value", s.number()))
use branch <- result.try(
s.required(s.record(Branch), "children", s.array(self)),
)
s.tagged("kind", [
s.variant("leaf", leaf, s.Strict),
s.variant("branch", branch, s.Strict),
])
})
}
recursive_pair(first_name, second_name, build) supplies two references, with
independent output types. Return Ok(#(first_body, second_body)); the result
contains both finalized definitions, each usable as a root. Recursion supports
self-recursion and pairs, not arbitrary heterogeneous recursion groups.
Every cycle must descend into an array item, dictionary value, or record field.
Direct reference loops, loops through nullable/union/annotation wrappers, and
mutual aliases without child descent are construction errors. This is a
conservative syntactic guard, not a proof that every definition has an accepted
value. Names use the same restricted syntax as named; pair names must differ.
Conflicting definitions sharing a name are rejected during graph finalization or
when composed definitions are emitted/validated.
Builder plans cannot be decoded, validated, emitted or rendered, including after
construction completes. Add defaults containing recursive values after their
definitions are completed. Nested construction cannot close over an unfinished
outer reference; use recursive_pair for that relationship. Use lift(completed)
to compose an existing definition inside a builder. Generic helpers accept
Plan(a, r) and return plans with the same r; see the
0.2.0 migration guide for annotated examples.
Schemas emit finite local $defs/$ref graphs. Decoding follows the same
references, preserving typed values and nested error paths. Documentation renders
references as links rather than expanding recursive trees. There is no hidden
depth limit. Stack exhaustion and callback exceptions remain operational failures,
not validation rejections.
Optional HTML documentation
gargamelle/documentation renders annotated definitions as collapsible HTML
fragments. Applications that only decode or validate data need not import it.
Headings show the type and, for record fields, REQUIRED or OPTIONAL.
The STRICT badge means that only the listed fields are allowed.
Dictionaries use the type label dictionary, with their value contract beneath.
Bounds, patterns, uniqueness, defaults and extra-field projection rules appear
below the heading when applicable.
A nested record with the reference stylesheet, with settings expanded and the dictionary and array collapsed:
import gargamelle as s
import gargamelle/documentation
import gleam/io
pub type Settings {
Settings(label: String, notifications: Bool, theme: String)
}
pub type Preferences {
Preferences(
enabled: Bool,
metadata: List(#(String, String)),
name: String,
settings: Settings,
tags: List(String),
)
}
pub fn preferences_definition() {
let settings =
s.record(fn(label) {
fn(notifications) { fn(theme) { Settings(label, notifications, theme) } }
})
let assert Ok(settings) = s.required(settings, "label", s.text())
let assert Ok(settings) = s.required(settings, "notifications", s.boolean())
let assert Ok(settings) = s.required(settings, "theme", s.text())
let fields =
s.record(fn(enabled) {
fn(metadata) {
fn(name) {
fn(settings) {
fn(tags) { Preferences(enabled, metadata, name, settings, tags) }
}
}
}
})
let assert Ok(fields) = s.required(fields, "enabled", s.boolean())
let assert Ok(fields) = s.required(fields, "metadata", s.dictionary(s.text()))
let assert Ok(fields) = s.required(fields, "name", s.text())
let assert Ok(fields) =
s.required(fields, "settings", s.finish(settings, s.Strict))
let assert Ok(fields) = s.required(fields, "tags", s.array(s.text()))
s.finish(fields, s.Strict)
}
pub fn main() {
io.println(documentation.render(preferences_definition(), "preferences"))
}
render(definition, id) returns an <article> fragment. Give each article a unique
ID. render_emitted(schema_json, id, aliases)
accepts already-emitted Gargamelle schema JSON and returns Result(String, String).
This is a renderer for Gargamelle’s schema vocabulary, not a general JSON Schema
validator or documentation engine for arbitrary schema dialects.
Descriptions, field labels, constants, defaults and IDs are HTML-escaped. Object
fields, array items, dictionaries and union alternatives retain their nesting.
Shared $defs render once with local links; arbitrary discriminator names work.
Optional aliases are (omitted_definition, displayed_definition) pairs. They must
refer to existing definitions and may not chain. They only compress presentation;
the caller must explain any omitted distinctions.
Create a public directory. Copy the stylesheet and script from
Renderer assets into
public/reference.css and public/reference.js. These optional assets provide
styling and expand/collapse controls; they do not require repository access.
Save the Gleam example as src/preferences_reference.gleam. Generate its article:
gleam run --target javascript --module preferences_reference > public/preferences.html
Save this complete page as public/index.html, replacing the marked comment with
the contents of public/preferences.html. Insert the article as HTML rather than
escaping it:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Preferences reference</title>
<link rel="stylesheet" href="reference.css">
<script src="reference.js" defer></script>
</head>
<body>
<header><h1>Preferences reference</h1></header>
<div class="controls">
<button type="button" data-fold="expand">Expand all</button>
<button type="button" data-fold="collapse">Collapse all</button>
</div>
<main>
<!-- Insert the rendered article here. -->
</main>
</body>
</html>
Open public/index.html in a browser. The stylesheet and script run in that local
page; the preview above is a static screenshot.
Native <details> folding needs no JavaScript. The optional script implements
the expand/collapse buttons, expands branches for printing, and restores their
previous state afterward. A host can supply its own stylesheet or embed the
fragment in an existing page. Page title, navigation, article grouping and
surrounding prose belong to the caller.
Fixed-position arrays
tuple starts an opaque builder with a curried constructor. Each element
consumes one constructor argument; finish_tuple closes the array at exactly
that length. The result may be a Gleam tuple, a custom type, or another typed
value:
let entry = gargamelle.tuple(fn(label) { fn(weight) { #(label, weight) } })
|> gargamelle.element(gargamelle.text())
|> gargamelle.element(gargamelle.number())
|> gargamelle.finish_tuple
// ["x", 1.25] decodes to #(String, Float); shorter/longer arrays are rejected.
An empty builder accepts only [] and returns its supplied constructor value.
Emission uses prefixItems, items: false, and minItems equal to the length;
the empty tuple omits prefixItems to remain meta-schema valid. Positional errors
use paths such as /1. Positions guard recursive edges by descending into the
input. Conversion callbacks run only after the whole input passes validation.
There are no optional trailing positions or rest elements. Use a disjoint union
of exact-length definitions, mapped to one output type, for two alternative
lengths. bounds and unique_items remain homogeneous-array operations and
reject tuple nodes, including tuples mapped to lists.
Object key constraints
property_names(object_definition, key_definition) returns a construction
result with the same decoded output type as the original object. For example,
a translation dictionary can require locale-shaped keys:
let assert Ok(locale) = gargamelle.pattern(
gargamelle.text(), "^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$",
)
let assert Ok(translations) = gargamelle.property_names(
gargamelle.dictionary(gargamelle.text()), locale,
)
The key definition has output type String, but only its structural validation
is used; its conversion and mapping callbacks never run. Original dictionary
keys and value decoding are preserved. Every present own key is checked,
including undeclared fields in open records. Empty objects have no key checks.
Absent optional or defaulted record fields do not introduce input keys to check.
Key failures use the property’s escaped JSON Pointer path, so a key and its value
can produce separate errors at the same path.
Repeated calls intersect the key contracts using propertyNames: {allOf: ...}.
Object type, value contracts, and property-count bounds remain enforced. Named
and annotated dictionary/record definitions are supported; arbitrary unions,
nullable objects, and unfinished recursive object placeholders are rejected by
this constructor. Apply constraints to the dictionary body inside a recursive
builder instead. Key definitions themselves may use names, unions, and maps.
patternProperties and schema-constrained extra record fields are not supported.
A mapped numeric definition may legally have output type String while retaining
its numeric acceptance schema. Used for property names, it accepts only empty
objects because actual names are strings. The key definition must accept strings
to allow nonempty objects; its decoded output type alone does not establish that.
Array membership
contains(array_definition, matching_definition, minimum, maximum) returns a
construction result with the original decoded output type. minimum is a
nonnegative safe integer; maximum is None or an inclusive safe integer at
least that minimum. For example, require one or two integer-valued elements
while preserving all decoded numbers:
let assert Ok(values) = gargamelle.contains(
gargamelle.array(gargamelle.number()), gargamelle.safe_integer(), 1, Some(2),
)
Every item must still satisfy the original array/tuple contract. Matches count
positions, including duplicates, rather than distinct values. A match uses only
structural validation; its conversion and mapping callbacks never run. The
original converter runs only after the complete definition accepts the input.
The matching definition may have a different output type from an array element.
Count failures use contains_count at the array’s JSON Pointer path.
Repeated calls intersect their membership requirements, emitted as separate
allOf branches. Zero minimum permits no matches; maximum zero requires none.
Zero minimum with no maximum is vacuous and emits no membership keywords, which
avoids emitting an unused contains keyword. Its matching definition still
participates in reference/name validation.
Array and fixed-tuple bodies, names, descriptions, titles, and existing unique arrays are supported. Nullable arrays and arbitrary unions are rejected as base contracts. Apply bounds and uniqueness before membership constraints when those operations require an unwrapped homogeneous array. Recursive membership checks consume array items; no depth cap is introduced. Complex overlapping body and match schemas may repeat validation work and increase decoding cost.
A content array can require at least one non-whitespace text item using a
matching text definition with the pattern \S. Other items still follow the
base item definition. If items include reference objects, checking whether those
references exist belongs in application code after decoding.
Structural conditionals
if_then(base, condition, consequence) applies the consequence when the
condition accepts the input. if_then_else(base, condition, consequence, alternative) validates exactly the selected branch. Both return a definition
with the base’s original output type; the auxiliary definitions may have
independent output types. For example:
let assert Ok(positive) = gargamelle.number_bounds(
gargamelle.number(), Some(gargamelle.Exclusive(0.0)), None,
)
let checked = gargamelle.if_then(
gargamelle.number(), positive, gargamelle.safe_integer(),
)
// Positive numbers must be safe integers; nonpositive finite numbers remain valid.
All checks inspect the original admitted JSON. Defaults, projection, or mapping
from the base decoder do not change condition selection. Auxiliary conversion
callbacks never run. Only the base converter runs, after the base and selected
requirements all accept. Unselected branch data errors are not reported; condition
errors select else or impose no further requirement when there is no else.
Operational exceptions still escape and are not treated as condition failure.
Selected-branch errors preserve their ordinary categories and input paths.
Repeated calls intersect their requirements. Emission uses allOf with the
base schema and separate if/then/optional else schemas, so nested conditions
cannot overwrite earlier ones. General typed intersections are not supported. Primitive modifiers, array/object constraints, and defaults
should be defined on the base before adding conditionals; existing modifiers
reject unsupported wrapper nodes rather than dropping conditional checks.
For object inspection, use required fields to make presence part of the condition. Open inspector records can require selected fields without rejecting other fields of the base input. A closed inspector imposes its full closed shape. A failed condition does not itself reject input, but the base must still accept. All condition and branch references participate in graph finalization even when a branch cannot be selected. Same-input recursive cycles are conservatively rejected, including unreachable branches; no satisfiability analysis is performed.
A settings record can require a sequence field when showPreview is explicitly
true. Use an open inspector record requiring showPreview with a True constant
as the condition, and another requiring sequence as the consequence. A decoder
default of true on an absent flag does not trigger this input-level condition.
Structural exclusion
exclude(base, forbidden) requires the base to accept and the forbidden
structural definition to reject the original admitted input. It returns the
base’s output type unchanged. The forbidden definition may have any output type;
its converters and mappings never run. For example, exclude safe integers
from a number definition:
let non_safe_integer = gargamelle.exclude(gargamelle.number(), gargamelle.safe_integer())
This example excludes only safe integers; it does not impose arbitrary-precision numeric semantics or reject every integer-valued IEEE-754 number. Exclusion always follows the exact structural contract supplied by the caller.
Repeated exclusions intersect their requirements. Emission conjoins the base
and each separate not schema in allOf, preserving earlier exclusions and
annotations. Exclusion failures use excluded at the input path. The forbidden
schema’s own failures are not reported: its rejection is the condition for
passing this constraint. Operational exceptions still escape rather than being
interpreted as rejection. Base conversion runs only after all validation passes.
Defaults and projections do not affect exclusion selection because checks run on original JSON. All references are collected even if the forbidden definition could never accept a valid base input. Same-input recursive cycles, including cycles through exclusion, are conservatively rejected. Excluding a definition from itself can legitimately produce an unsatisfiable contract; construction does not solve satisfiability. Apply primitive modifiers to the base before exclusion.
An identifier can use a nonempty text definition as its base and exclude strings matching whitespace. Uniqueness across separately decoded records remains an application check.
Checked documentation examples
documentation.render_examples(definition, id, examples) accepts admitted JSON
values and returns HTML only when all examples validate against the current
definition. It returns every rejected example’s zero-based index and ordinary
validation errors. It never decodes examples or executes mapping callbacks.
Examples represent input JSON, including its original missing fields, rather
than projected/defaulted outputs. HTML escapes examples, and an empty example
list produces the ordinary renderer output. Rechecking at render time catches
examples that become invalid after a definition changes.