Gargamelle technical contract
This document specifies the intended public semantics of Gargamelle 0.1.0. “Must” denotes a requirement, not a claim that the implementation has been formally proved. Verification results and implementation history are separate.
1. Model and scope
A Definition(a) consists conceptually of a structural acceptance relation and
a conversion from accepted JSON inputs to values of type a. Public definitions
are opaque and constructible only through library operations. The definition is
authoritative: schema emission and decoding are derived interpretations of it.
There is no independent schema/decoder override or arbitrary predicate escape hatch.
The supported target is Gleam compiled to JavaScript. Schemas use JSON Schema Draft 2020-12 and declare that dialect at the root. The contract applies to the vocabulary generated by the public constructors, not arbitrary imported schemas.
Let J be finite JSON trees represented after JavaScript JSON parsing: null,
booleans, strings, finite IEEE-754 binary64 numbers, arrays and objects with
string keys. Values containing undefined, functions, bigint, NaN, infinities,
cyclic references, accessors or arbitrary host objects are outside J. Public
Value is opaque; parse admits JSON text into this domain.
Parsing follows JSON.parse semantics, including rounding numeric literals,
underflow and retaining the last occurrence of a duplicate object key. Numeric
overflow to a nonfinite value is an admission error. The contract concerns the
resulting parsed value, not arbitrary-precision decimal text. Escaped lone
surrogates follow JavaScript string semantics.
2. Acceptance equivalence and value correctness
For every well-formed, finalized definition S and x in J, assuming sufficient runtime resources and pure, total conversion callbacks:
Valid202012(emit_json(S), x)
⇔ validate(S, x) = Ok(Nil)
⇔ ∃v. decode(S, x) = Ok(v)
Valid202012 denotes the dialect’s validation semantics without coercion,
default insertion or property removal. Every comparison must use the same
parsed input value. A validator’s optional lint restrictions are not additional
acceptance rules.
For accepted input, the returned v must also satisfy S’s decoded-value contract below. Acceptance agreement alone is insufficient. For invalid in-domain input, validation and decoding must return structural data errors. They must not treat callback exceptions or runtime exhaustion as rejection.
validate and emission must not execute conversion callbacks. decode must
validate the complete input before conversion. Construction must not probe
constructors or maps with fabricated values. Recursive builders receive typed
reference definitions, not invented decoded outputs. Neither validation nor
conversion performed by the library mutates the admitted input.
Mappings are trusted application functions. They cannot add structural rejection rules. Exceptions, nontermination and side effects in user functions are outside the guarantee. Application checks such as cross-record identity, external references and business invariants belong in a separate semantic validation stage.
3. Constructor semantics
The identifiers below describe semantic operations; public API spellings are listed in the generated API reference. All composition must preserve section 2.
| ID | Construct | Schema/acceptance contract | Decoded-value contract |
|---|---|---|---|
| P01 | Boolean, null | Exactly the corresponding JSON type | Preserve value |
| P02 | String | String only; optional inclusive min/max length and pattern | Preserve string exactly; no trimming, case changes, or Unicode normalization |
| P03 | Safe integer | integer plus explicit bounds within -9007199254740991…9007199254740991; optional tighter inclusive bounds | Preserve mathematical integer; accept parsed 1.0 as 1; -0 and 0 are equivalent |
| P05 | Finite number | number; optional finite inclusive/exclusive endpoints, intersected on repetition | Preserve the admitted binary64 value as Float, including signed zero; comparison treats both zero signs equally |
| P04 | String enum; string, safe-integer, Boolean and null constants | Exact membership/equality, without coercion | Preserve value; optional mapping follows A02 |
| C01 | Homogeneous array | Array; every item satisfies its child; optional inclusive min/max item count | Preserve order and duplicates; decode every element |
| C02 | Record | Object; declared required/optional fields; explicit extra-field policy | Decode declared fields by wire name and combine into a typed value |
| C03 | String-key dictionary | Object; every value satisfies its child; optional inclusive min/max property count | Preserve every key and decode each value; key order has no semantic meaning |
| C04 | Nullable child | Child accepts OR input is null, including nullable enums/constants/unions | Option(a): None for null, Some(decoded child) otherwise; outer null check takes precedence |
| C05 | Any-of | At least one child accepts | Select the first successful branch in declaration order |
| C06 | One-of | Exactly one child accepts | Decode that unique branch; reject overlapping matches |
| C07 | Tagged union | Object branches with required, distinct string-constant discriminators; equivalent to their emitted one-of | Select the matching variant; discriminator is consumed, not duplicated in branch output |
| C13 | Structural exclusion | Base accepts and forbidden definition rejects the original input | Retain base output; forbidden converters never run |
| C12 | Structural conditional | Base accepts; if condition accepts, consequence accepts; otherwise optional alternative accepts | Retain base output; auxiliary converters never run |
| C11 | Array membership | Typed array/tuple body and inclusive count limits on structurally matching positions | Retain original output; never invoke matching converters |
| C10 | Property names | Dictionary/record input; each present own key satisfies every supplied string-output key definition | Retain original output and keys; never invoke key converters |
| C09 | Exact-length tuple | Array of exactly N elements; position i satisfies child i | Apply the curried typed constructor in positional order |
| C08 | Guarded recursion | Local $defs/$ref graphs; every cycle must descend through an item, tuple position, dictionary value, or record field | Preserve the declared recursive output type; mutually recursive pairs may have distinct output types |
| A01 | Description/title | Annotation only; no change in accepted inputs or decoded values | Preserve child result |
| A02 | Total map | No change to schema or acceptance | Apply mapping to successfully decoded value; mapping cannot add rejection rules |
4. Primitive and constraint rules
Safe integers use inclusive bounds within ±9007199254740991. These limits must
appear in the emitted schema. Parsed 1.0 is integral; integer comparisons treat
negative zero and zero equivalently. Finite numbers decode to Float and preserve
the admitted binary64 value, including signed zero. Number constraints use finite
inclusive/exclusive endpoints. Repetition intersects intervals; equal endpoints
with either side exclusive and inverted intervals fail construction. A valid
real interval need not contain a representable binary64 value.
String values are preserved without trimming, case conversion or normalization.
Lengths count Unicode code points, not UTF-16 units or grapheme clusters. Patterns
use ECMAScript Unicode (u) regular expressions with search semantics and no
implicit anchoring or additional flags. Invalid expressions fail construction.
Distinct repeated patterns are conjoined; identical patterns are deduplicated
in first-declaration order. Regex execution bounds are not guaranteed.
Integer bounds and size bounds are inclusive. Size limits are nonnegative safe
integers. None denotes an unbounded endpoint. Repetition intersects bounds;
invalid bounds and application to unsupported underlying definitions fail
construction. Bounds/patterns should be applied before naming or union composition;
wrappers are not a promise that every constraint supports every composition.
Enums are nonempty, contain distinct strings and perform no coercion. Constants use type-sensitive equality. Integer constants must lie in the safe range.
unique_items applies to a direct homogeneous array definition and compares the
original JSON items structurally, before mapping. Object key order is irrelevant
and both numeric zero signs are equivalent. Decoded list order is unchanged.
5. Records, presence and tagged variants
Fields are static declarations keyed by wire name. A curried constructor consumes
actual decoded field values. Duplicate wire names fail construction. A record
explicitly chooses Closed (reject extras) or Open (accept extras and project
them away from output). Empty records still require objects. Dictionaries retain
all keys and decode their values to a Gleam list of key/value pairs; key order has
no semantic meaning. Keys including __proto__, constructor, toString, empty
strings and Unicode must be handled as data.
Presence is independent of nullability. For child output a:
| Field | Missing | Present null | Present non-null |
|---|---|---|---|
| Required | Reject | Child decides | Decode child |
| Optional | None | Some(child output) if accepted, otherwise reject | Some(child output) |
| Optional nullable | None | Some(None) | Some(Some(child output)) |
| Defaulted | Decode declared default | Child decides; do not substitute | Decode child |
Defaults are admitted JSON values validated during construction without invoking
converters. On absence, conversion decodes that default normally. Present invalid
values never fall back to defaults. The schema’s default remains an annotation;
no input or external-validator mutation is permitted.
Nullable uses an outer-first null check: null produces None, otherwise the
child result is wrapped in Some. Nested nullable values may leave some output
values unreachable. Null bypasses an inner one-of, even if multiple inner branches
accept null. Non-null child errors retain their paths.
A tagged union owns a required string discriminator with distinct tags. Branches come from record builders; arbitrary wrapped definitions are not branches. The discriminator must not be redeclared. Variant annotations and maps are explicit. The selected branch constructs its ordinary payload; the tag is not injected as an extra output field. Empty variant sets and duplicate tags fail construction. Missing, invalid or unknown tags are diagnosed before branch fields.
6. Structural auxiliary constraints
Auxiliary definitions contribute only their structural acceptance relations; their conversion callbacks must never execute. Base output and type are retained. Checks inspect the original admitted input before defaults, projection or maps.
property_names constrains every present own object key as a JSON string and
conjoins repeated constraints. It accepts supported record/dictionary bodies.
Keys are not renamed. An empty object satisfies the key constraint vacuously.
A String-output mapping can retain a non-string structural contract; that may
reject every actual key while still accepting the empty object.
contains counts array positions satisfying a matching definition, including
duplicate positions, and enforces nonnegative inclusive minimum/optional maximum
counts. It supports homogeneous arrays and exact tuples. Repeated requirements
are conjoined. Minimum zero with no maximum adds no acceptance restriction.
It neither removes unmatched elements nor alters conversion.
if_then requires the consequence only when the condition accepts. if_then_else
requires the consequence on condition acceptance and the alternative otherwise.
Condition rejection is branch selection, not a data error. Operational failure
must not select the alternative. Repeated conditionals are conjoined.
exclude requires the base to accept and every forbidden definition to reject.
A matching forbidden definition produces a data error at the current path.
Operational failure is not evidence of forbidden rejection. Repetition intersects
constraints without replacing previous exclusions.
7. Names and recursion
Names start with an ASCII letter and otherwise contain only ASCII letters,
digits, underscores or hyphens. Named definitions preserve acceptance and output
while emitting finite local $defs/$ref graphs. Distinct bodies with conflicting
names, including transitive conflicts, must not produce an ambiguous schema.
Self recursion and mutually recursive pairs use typed placeholders and retain the declared output types; the pair’s types may differ. Every cycle must descend into a strict input subvalue through an array item, tuple position, dictionary value or record field. Names, references, nullable, union, annotations and same-input constraints do not supply descent. Same-input cycles fail construction, including syntactically reachable cycles in otherwise unreachable branches. The guard is conservative; it is not a satisfiability proof.
References cannot be validated, decoded, emitted or used to validate defaults before finalization. Rejected graph finalization invalidates its bound references. Unfinished or conflicting composed graphs fail operationally at public validation or emission boundaries. Traversal of finalized schemas must terminate through identity tracking; shared completed branches must not be repeatedly expanded by the recursion guard. Emission must remain finite even for recursive definitions.
There is no implicit decoding depth cap, input-size cap or definition-size cap in acceptance. Stack exhaustion, timeouts and other resource failures remain operational failures. Linear or bounded runtime is not guaranteed for arbitrary compositions, overlapping branches or patterns.
8. Schema emission and canonical serialization
emit_json returns an opaque JSON value describing the definition;
emit_text returns its canonical JSON string. Emission must not depend on
conversion callbacks or process history. The schematic forms are:
| Construct | Emitted form (schematic) |
|---|---|
| Boolean / null | {"type":"boolean"} / {"type":"null"} |
| String | {"type":"string"} plus supplied minLength, maxLength, pattern |
| Safe integer | {"type":"integer","minimum":effective_min,"maximum":effective_max}; always include safe bounds or tighter bounds |
| String enum | {"type":"string","enum":[declared values]} |
| Constant | {"type":corresponding_type,"const":value}; safe-integer constants use type integer |
| Exclusion | allOf containing the base and separate not schemas |
| Conditional | allOf containing the base and separate if/then/optional else schemas |
| Membership | Array body plus contains, explicit minContains, optional maxContains; repeated requirements use allOf; vacuous zero/no-maximum requirement omitted |
| Property names | Object body plus propertyNames: key; repeated constraints use allOf; explicit object type beside named references |
| Tuple | {"type":"array","prefixItems":[children],"items":false,"minItems":N}; omit prefixItems for N=0 |
| Array | {"type":"array","items":child} plus supplied minItems/maxItems |
| Record | {"type":"object","properties":{...},"required":[...],"additionalProperties":true_or_false}; include empty properties/required |
| Dictionary | {"type":"object","additionalProperties":child} plus supplied minProperties/maxProperties |
| Nullable | {"anyOf":[child,{"type":"null"}]} unconditionally, including nested nullable, enum and null children |
| Any-of / one-of | {"anyOf":[children]} / {"oneOf":[children]} in declaration order; no flattening or deduplication |
| Tagged union | {"oneOf":[augmented branch records]} in declaration order |
| Required / optional field | Include / omit wire name in record required; presence alone does not change child schema |
| Defaulted field | Omit wire name from required; attach declared JSON default to its child schema |
| Description/title | Attach description/title to the child schema object; outermost repeated annotation wins |
| Total map | Emit child schema unchanged |
Finite numbers emit type: number with minimum/maximum or
exclusiveMinimum/exclusiveMaximum as appropriate. Unique arrays add
uniqueItems: true. Named/reference targets are collected into root $defs
and represented by local $ref without recursive expansion. No remote or
dynamic references or implicit $id are introduced.
The root declares $schema: https://json-schema.org/draft/2020-12/schema.
Metadata is added only by the corresponding constructors. Distinct repeated
string patterns emit allOf pattern branches; repeated membership, property-name,
conditional and exclusion constraints preserve conjunction. Zero/no-maximum
membership may omit vacuous keywords. Annotations do not change acceptance and
outermost repeated titles/descriptions win.
Required-field arrays are sorted by lexicographic UTF-16 code-unit order; enum,
union and other semantically ordered arrays preserve declaration order. Canonical
text recursively inserts object keys in that sorted order and uses compact
JSON.stringify serialization without a trailing newline. JavaScript enumeration
then places array-index keys first in numeric order. Thus properties named 2
and 10 serialize in numeric order, while the required array is ["10","2"].
Defaults obey the same canonicalization. All own keys must survive serialization
without invoking inherited setters or changing prototypes.
For the same finalized definition, text emission must be byte-identical:
parse(emit_text(S)) = Ok(emit_json(S)) // JSON value equality
emit_text(S) = emit_text(S) // byte equality on repeated emission
JSON object member order is not part of value equality. Serialization normalizes negative zero to zero. There is no typed encoder, arbitrary schema importer, surjectivity guarantee onto the output type or decode/encode inversion promise. Defaults, projection and maps can deliberately lose the original representation.
9. Errors and boundaries
Admission errors concern JSON syntax and nonfinite parsed values. Construction errors concern invalid definitions: duplicate fields/tags/enums, empty enums or unions, invalid bounds, constants, defaults, names, regexes or recursion graphs, and unsupported constraint/definition combinations. Some invalid combinations are excluded by static types. Data errors concern admitted values rejected by a well-formed definition. Operational failures are distinct from all three.
Data paths are JSON Pointers: root is the empty string, array indices are decimal,
~ becomes ~0 and / becomes ~1. Missing required fields point to the containing
object and carry missing_key; undeclared keys point to that key. Errors have
categories. Union errors carry zero-based branch failures for no match and
matching indices for one-of ambiguity. Selected tagged-branch errors pass through;
nullable non-null child errors preserve their paths. English wording and error
enumeration order are not guarantees.
Core decoding and emission have no filesystem, network, application storage, UI or host-lifecycle dependencies. Runtime helpers may implement primitive JSON, regex, numeric and graph operations; they must not forge generic typed values.
10. Optional documentation
HTML documentation is presentation of the emitted vocabulary, not an independent validator. Text and attribute values must be escaped. Shared and recursive references are represented finitely with links. Presentation aliases do not change schema or decoding semantics.
Checked examples are admitted input values validated against the definition, without executing converters. All rejected example indices and validation errors are returned instead of rendering a successful example section. Accepted examples are displayed as escaped canonical input JSON, not as decoded outputs. An empty example list has the ordinary rendering behavior.
11. Excluded guarantees
The contract does not claim full JSON Schema dialect support, arbitrary schema import, remote/dynamic references, arbitrary heterogeneous recursion groups, general public intersection, pattern properties, schema-constrained extra record fields, format assertions, multipleOf, fractional/general JSON constants, encoders, migrations or reversible value conversion. Erlang and other runtime support are outside the 0.1.0 contract. Machine-checked correctness, resource bounds and arbitrary user-code safety are not promised.