Gargamelle
Gargamelle generates a typed JSON decoder and a JSON Schema 2020-12 document from one Gleam definition.
See the technical contract.
Gargamelle also provides boundary APIs that bind a definition to a handler. Incoming JSON is validated against that definition before the handler runs. Handlers receive decoded Gleam values and the original JSON, or verified JSON without decoding.
Boundary APIs are optional; definitions, decoding and schema generation can be used directly.
Definitions can also generate browsable HTML documentation.
| Operation | API |
|---|---|
| JSON decoding into a Gleam value | decode |
| JSON Schema generation | schema_json / schema_string |
| Validated handler invocation with decoded data | boundary → Boundary + forward |
| Validated handler invocation without decoding | json_boundary → JsonBoundary + forward_json |
| JSON retention with the accepting definition | verify → VerifiedJson |
Getting started
Requires Gleam 1.18.1 or later.
Package installation:
gleam add gargamelle@0.4.0
JavaScript
Target configuration in gleam.toml:
target = "javascript"
Erlang
Target configuration in gleam.toml:
target = "erlang"
The native regex helper requires make and a C compiler. Helper compilation:
gleam deps download
make -C build/packages/gargamelle/priv/regex-src
Native Erlang setup documents tested runtimes and deployment requirements.
Basic example
import gargamelle
import gleam/io
import gleam/string
pub fn main() {
let definition: gargamelle.Definition(List(String)) = gargamelle.array(gargamelle.text())
let schema: String = gargamelle.schema_string(definition, gargamelle.Pretty)
let assert Ok(json): Result(gargamelle.Json, _) = gargamelle.parse("[\"Brainy\", \"Jokey\"]")
let assert Ok(data): Result(List(String), _) = gargamelle.decode(definition, json)
io.println(schema)
io.println(string.join(data, ", "))
}
Output:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"items": {
"type": "string"
},
"type": "array"
}
Brainy, Jokey
Definition(List(String)) describes both accepted JSON and decoded output.
parse returns opaque Json; decode produces List(String). The Result
annotations describe the whole Ok(...) pattern; _ leaves the error type inferred.
schema_string returns the schema as a String. Pretty uses two-space
indentation; Compact produces canonical JSON. to_string(json, format) serializes
any Gargamelle Json, not just schemas.
Parsing and decoding errors are separate results. The example uses assertions
for brevity; application code handles those errors explicitly. validate checks
acceptance without constructing the decoded output.
Receiving boundaries
A boundary binds a handler to its input definition. Submission validates JSON against the stored definition and invokes the handler only on success, combining validation and handler invocation in one operation.
This complete example uses the same array-of-strings definition for two receivers:
import gargamelle
import gleam/io
import gleam/string
pub fn main() {
let names: gargamelle.Definition(List(String)) = gargamelle.array(gargamelle.text())
let assert Ok(json): Result(gargamelle.Json, _) = gargamelle.parse("[\"Brainy\", \"Jokey\"]")
let receiver = gargamelle.boundary(
names,
fn(decoded_names, _original_json) { string.join(decoded_names, ", ") },
)
let assert Ok(response): Result(String, _) = gargamelle.forward(receiver, json)
io.println(response)
let json_receiver = gargamelle.json_boundary(
names,
fn(verified) {
gargamelle.to_string(gargamelle.verified_json(verified), gargamelle.Compact)
},
)
let assert Ok(response): Result(String, _) = gargamelle.forward_json(json_receiver, json)
io.println(response)
}
Output:
Brainy, Jokey
["Brainy","Jokey"]
Both boundaries check their stored definition before invoking the handler.
Boundary also decodes and passes the typed data and original JSON;
JsonBoundary delivers a VerifiedJson certificate without running decoding
conversions. Invalid input does not invoke either handler.
VerifiedJson(a) retains the original admitted JSON and the exact definition that
accepted it. It does not contain a decoded a. The type parameter identifies the
definition’s output type, not its particular constraints: two definitions producing
String can accept different inputs. Receiving boundaries always check their own
stored definition; a certificate from another definition cannot bypass that check.
boundary_definition and json_boundary_definition expose the receiving
definition for inspection and schema generation.
Verified JSON boundaries
documents certificate retention and application responses.
Guarantees and scope
- Validation, decoding acceptance and the generated schema describe the same structural rules within the documented JSON domain and validator settings.
- Receiving boundaries validate before invoking a handler; invalid input runs neither decoding conversions nor the handler.
- Application rules, such as whether a score agrees with its answers, need separate checks. A definition does not automatically express those rules.
- Conversion callbacks must be pure and total. Conversion or handler exceptions and resource failures remain operational failures, rather than validation errors.
- The opaque Gleam APIs enforce access through public operations. They are not a sandbox against hostile code in the same runtime.
The technical contract specifies the JSON domain, external validator settings, decoded-value semantics and limits.
Typed records
import gargamelle as s
pub type User { User(name: String, active: Bool) }
pub fn user_definition() -> s.Definition(User) {
let assert Ok(fields) =
s.record(fn(name) { fn(active) { User(name, active) } })
|> s.required("name", s.text())
let assert Ok(fields) = s.required(fields, "active", s.boolean())
s.finish(fields, s.Strict)
}
pub fn read_user(source: String) {
let assert Ok(json) = s.parse(source)
s.decode(user_definition(), json)
}
For input {"name":"Brainy","active":true}, read_user returns
Ok(User("Brainy", True)). s.Strict rejects undeclared fields.
s.schema_string(user_definition(), s.Pretty) generates the record’s schema.
More definitions
| Feature | Constructors |
|---|---|
| Recursive structures | recursive, recursive_pair |
| Tuples and array membership | tuple, element, contains |
| Object key and presence constraints | property_names, dependent_required |
| Conditional constraints and exclusion | if_then, if_then_else, exclude |
| Numeric constraints | bounds, number_bounds, integer_multiple_of |
| Schema metadata | title, description, examples, default, format |
| Explicit UUID validation | assert_format(_, Uuid) |
Metadata does not change acceptance. A schema default annotation does not insert
a value; a defaulted record field is a separate decoding feature. format is an
annotation, while assert_format adds validation constraints.
Advanced definitions documents these features and the HTML documentation recipe.
Limitations
JavaScript is tested on Node 24.10.0, Chrome 155, Firefox 157 and Safari 27. Native Erlang requires a C regex helper and is tested on macOS arm64 with OTP 29 and Ubuntu 24.04 x86_64 with OTP 28.5.0.7 and 29.1.1. Browser compatibility and Native Erlang setup document the compatibility matrix, regex Unicode differences and deployment requirements. Other runtimes and platforms are unverified; Windows native support is unavailable.
- Gargamelle supports the JSON Schema features exposed by its constructors. It does not validate arbitrary imported schemas or implement the full 2020-12 dialect.
- JSON numbers are finite parsed binary64 values;
safe_integeruses JavaScript’s safe-integer range. Strings and object keys must contain Unicode scalar values. - General typed intersections, fractional divisibility and fractional constants
are unsupported.
integer_multiple_ofsupports exact safe-integer divisibility; UUID is the only supported explicit format assertion. - Runtime resource use is not bounded. Parsing may round number literals and collapse duplicate object keys.
- There is no typed encoder, automatic decode/encode inverse or migration API. Defaults, projection and maps can lose information during decoding.
APIs may change before 1.0.
Related libraries
Gargamelle was inspired by glon.
Sextant also combines typed decoding with
JSON Schema 2020-12 generation. Its use-based field API supports definitions whose
later fields depend on earlier decoded values. Gargamelle uses static definitions
and curried record constructors; its schema describes structural acceptance, while
application rules are checked separately. Receiving boundaries bind those definitions
to handlers.
Further documentation
- Technical contract: complete intended acceptance, output, emission and error semantics.
- Advanced definitions: numeric constraints, recursion, tuples, key constraints, membership, conditionals, exclusion and HTML documentation.
- Verified JSON boundaries: typed ingress and JSON-only receiving handlers.
- Migrating to 0.4.0: boundary APIs and added schema constraints.
- Migrating to 0.3.0: required API renames.
- Migrating to 0.2.0: string requirements and changes to recursive construction.
Verification
The library is checked with typed-output and rejection fixtures, generated compositions, independent JSON Schema validators, selected official suite cases and deliberate implementation mutations. These provide evidence for the contract, not a formal proof or full-dialect conformance. The package source is available in the Hex archive.
License
Gargamelle is distributed under the MIT License. Licenses contains the library, native regex engine and retained JSON Schema test-suite notices.