14. JSON Schema export: the bridge out, and the loss report
JSON Schema as the bridge out: MCP inputSchema, OpenAPI, stock validators
Exercises jsonschema --at/--strict/--format json, the stderr loss report, exit classes, the money-wire const mark
Rendered from
use-cases/14-jsonschema-export/README.md
in the engine repository. The models, the expected output and the
check.sh that drives the CLI over all of it are in
use-cases/14-jsonschema-export/.
The worked example of
aontu jsonschema. JSON
Schema is what the rest of the world reads: an MCP tool’s
inputSchema must be one, every major structured-output API
constrains generation to one, OpenAPI embeds one, stock validators
check one. The verb exports the unified value as draft 2020-12 to
stdout and names every loss on stderr. This case drives the bridge in
its three moods: exact, lossy, refused.
The model
Four documents, one per mood plus the money convention:
- registry.aon: a three-tool MCP-flavoured registry (use-case
09’s shape, self-contained), written in the subset that crosses
without loss, so each per-tool export is complete.
jsonschema --at '$.argschemas.<tool>'answers the tool’sinputSchemadirectly, with nothing on stderr. - message.aon: a wire message whose root is one
close()expression, so the whole-document export carriesadditionalProperties: falseat its root: pasteable into an OpenAPI components entry with nothing to strip. - money.aon: use-case 10’s money wire convention (the recipe is
Carry exact money over JSON).
The fixed-scale decimal’s
re()crosses aspatternand the constantdecmark as{"const": "bigdecimal:2"}outsiderequired, so a consumer reading only the JSON Schema learns the exact leaf and the scale. - residue.aon: one instance of each loss class:
must(),bigdecimal,hide(), a constrained spread template,length()on a list. The export still happens; every loss is named. - bad/dangling.aon: a reference that resolves nowhere. Not a loss: no unified value, no export, exit 4.
The line between registry.aon and residue.aon runs through two
constructs. A bare-kind template ([&: string], { &: string })
crosses as items or additionalProperties; a template carrying a
constraint call ({ &: string & length(max(63)) }) is held residual,
exports as {} in that position, and is reported as unresolved.
length() on a list exports as minItems/maxItems and is reported
as well, because a count has no domain until data arrives. One more
construct reports under a name other than its own: must() holds the
whole value residual, so number & must(...) exports as {} and is
reported as nil.
Every golden in expected/ is captured engine output.
The model tree
residue.aon is deliberately small and deliberately awkward: every
field of report is a construct the JSON Schema export must either
carry or drop, and the loss report says which. A bigdecimal, a
spread template, a list template, a concrete string and a nil.
$
└── report
├── amountEur bigdecimal
├── annotations {&:string&length(integer&min(...
├── attempts [&:integer]
├── audit "kept-off-the-wire"
└── total nil
aontu view doc --depth 3 residue.aon draws it, and check.sh pins it
with --out --check. A key with (n) after it is a container the
depth bound stopped at, and n is how many keys are not drawn; a
leaf carries its canon, which is the kind of thing it is rather
than its value.
What check.sh proves
- Three per-tool exports match their goldens with EMPTY stderr: inputSchema-shaped, closed, nothing lost.
- The exports hold under a stock JSON reader (python3): closedness,
the required list, an enum, and two
re()on one string rendered asallOfof patterns. - The whole-document message export: root
additionalProperties: false, disjunctions asenum, the*preference asdefault, optional keys out ofrequired. - The money convention crosses intact:
patternforDec2,constfor the mark, andrequiredstays["amount", "currency"]. - residue.aon exports at exit 0 while stderr names all five losses, each with its path and construct.
--strictflips the same run to exit 1.--format jsoncarries the same report as data:verdict: lossy, each loss as{path, construct, reason}, the schema embedded.- bad/dangling.aon refuses with exit 4 and a located
[aontu/no_path], and stdout stays empty: never a partial schema.
Run
./check.sh # all assertions
The Go CLI has the verb too, and the same checks run against it:
(cd ../../go && go build -o /tmp/aontu-go ./cmd/aontu)
AONTU=/tmp/aontu-go ./check.sh
Build the binary rather than using go run, which remaps the verb’s
exit 4 to its own exit 1 and fails check 8.