Advanced definitions and documentation

These examples extend the core record and array workflows. Acceptance and output semantics are specified in the technical contract.

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 value_text follows JSON serialization and normalizes negative zero to 0. The independent validators and decoder must receive the same parsed value.

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. multipleOf and fractional-number constants are not supported.

Recursive definitions

recursive(name, build) creates a guarded self-recursive definition. Its builder runs once with a typed definition reference, never a fabricated decoded value. The builder returns Result(Definition(a), ConstructionError). For example:

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.Closed),
      s.variant("branch", branch, s.Closed),
    ])
  })
}

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. This first API 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 references are scoped to construction. They cannot be decoded/emitted before finalization, nor used to validate a default while unresolved. A rejected construction invalidates its bound placeholders; escaped placeholders cause operational failures even in empty arrays. Add defaults containing recursive values after their definitions are finalized. Nested construction cannot close over an unfinished outer reference; use recursive_pair for that relationship.

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. It is independent of the decoding module: applications that only validate data need not import or ship it.

import gargamelle
import gargamelle/documentation

let name = gargamelle.text() |> gargamelle.description("Display name")
let fragment = documentation.render(name, "display-name")

render(definition, id) returns an <article> fragment. Give each article a unique ID. The renderer consumes Gargamelle’s emitted representation internally rather than exposing its private definition AST. 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. No domain-specific aliases are built into Gargamelle.

The supplied stylesheet and script are separate assets in the source repository; they are not included in the Hex package. Download the release-tagged files into the directory where you will save your HTML page (repository access is required):

mkdir -p public
curl -fL https://raw.githubusercontent.com/vistuleB/gargamelle/v0.1.0/documentation/reference.css -o public/reference.css
curl -fL https://raw.githubusercontent.com/vistuleB/gargamelle/v0.1.0/documentation/reference.js -o public/reference.js

Save this complete page as public/index.html, replacing the marked comment with the string returned by documentation.render in the example above. Insert that string 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>Display name reference</title>
  <link rel="stylesheet" href="reference.css">
  <script src="reference.js" defer></script>
</head>
<body>
  <header><h1>Display name 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. See the rendered sample for the article produced by this definition.

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 generalized additional-property handling remain deferred.

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. Ajv’s strictTypes lint refuses this valid unsatisfiable key contract. Verification retains an exact-schema exception for that audit fixture, explicitly disables only strictTypes, and compares Gargamelle with both Ajv and Hyperjump. The ordinary oracle remains strict; no automatic fallback or runtime behavior change is introduced.

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 also avoids Ajv’s strict-mode rejection of an ignored 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; there is no shared input cache.

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. There is no general typed allOf constructor in this extension. 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, forbid integer-valued numbers while retaining fractional Float values:

let fractional = 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.

✨ Search Document