Gargamelle

Package Version Hex Docs

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 contract to a handler. A contract combines a definition with optional semantic checks of decoded values. Incoming JSON must satisfy that contract before the handler runs. Handlers receive decoded Gleam values and original JSON, or verified JSON.

Boundary APIs are optional; definitions, decoding and schema generation can be used directly.

OperationAPI
Direct admitted JSON constructionjson_* constructors
JSON decoding into a Gleam valuedecode
Decoding or validation with semantic requirementsdecode_contract / validate_contract
JSON Schema generationschema_json / schema_string
Validated handler invocation with decoded databoundary → Boundary + forward
Validated handler invocation with original JSONjson_boundary → JsonBoundary + forward_json
JSON retention with the accepting contractverify → VerifiedJson
Typed JSON encodingencoding.encode_json
Encoding checked against a Contractencoding.encode
Typed output through a receiving boundaryencoding.output_boundary + encoding.send

Getting started

Requires Gleam 1.18.1 or later.

Package installation:

gleam add gargamelle@0.7.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 contract. Submission checks JSON against the stored contract 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(
    gargamelle.contract(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(
    gargamelle.contract(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 contract before invoking the handler. Boundary also decodes and passes the typed data and original JSON; JsonBoundary delivers a VerifiedJson certificate. Contracts without semantic checks validate without decoding; semantic checks require one decoding pass. Structural or semantic rejection invokes neither handler.

VerifiedJson(a) retains the original admitted JSON and the exact contract 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 contract; a certificate from another contract 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

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

FeatureConstructors
Arbitrary admitted JSONanything
Recursive structuresrecursive, recursive_pair
Tuples and array membershiptuple, element, contains
Object key and presence constraintsproperty_names, dependent_required
Conditional constraints and exclusionif_then, if_then_else, exclude
Numeric constraintsbounds, number_bounds, integer_multiple_of
Schema metadatatitle, description, examples, default, format
Explicit UUID validationassert_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.

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 structural definitions and semantic requirements to handlers.

Further documentation

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.

✨ Search Document