ADR-126: A named JSON-Schema subset, enforced strict
- Status
-
Accepted
- Date
-
2026-08-05
Context
ADR-082 shipped structured completions with a three-keyword structural matcher
— type, required, properties — and deliberately excluded enum,
pattern, oneOf and the rest: a full validator would add a runtime
dependency. The matcher is fail-open: unknown keywords are silently ignored,
an empty schema accepts everything.
That posture is correct for the paths the matcher also guards — the ADR-105
tool-input gate and the resume paths depend on its exact semantics — but wrong
for structured completions, where a caller who writes enum deserves either
enforcement or an error, never silent acceptance. The roadmap asked for
"enum, pattern, oneOf, full draft support".
Decision
The validator gains a second, STRICT mode next to the untouched lenient one.
Its contract is a named subset, owned by
Strict:
- Enforced:
type(incl. union arrays),enum,const,pattern, string lengths, numeric bounds (incl. numericexclusive*), integermultipleOf,items/prefixItemsand the array assertions,properties/required/additionalProperties(bool and schema forms),oneOf/anyOf/allOf/not. - Annotations accepted and ignored:
description,title,default,examples,format(annotation-only in 2020-12),$schema,$id. - Everything else — notably
$ref/$defs— is out of subset, and out of subset is fail-closed: the schema is rejected as a whole, so the subset is enforceable rather than aspirational. "Full draft support" is deliberately not built; reference resolution is the point at which a subset becomes a JSON-Schema implementation, and ADR-082's no-runtime-dependency decision stands.
complete pre-flights the schema against the subset and
throws (code 1784500003) before the first provider call: an out-of-subset
schema fails for every possible response, and discovering that after the
repair round-trip would cost two paid requests.
The lenient mode is byte-identical to before. The two modes are tied together by an invariant a fuzzy test pins: anything strict accepts, lenient accepts. Strict only ever rejects more, which is what makes it safe to introduce next to a mode that guards a security boundary.
Named deviations from JSON Schema 2020-12
Each deliberate, none silent:
patternis compiled as PCRE (delimiters added,umodifier), not ECMA-262. A non-compiling pattern is out of subset; a match aborted by the backtracking limit rejects — fail-closed either way.multipleOfis honoured for positive integers only. Float steps need epsilon arithmetic that quietly lies about conformance (fmod(0.3, 0.1)≠ 0).5.0is not an integer. Primitive matching byte-mirrors the lenient matcher's PHP-native checks — the price of the strict-implies-lenient invariant, and cheaper than two subtly different type systems.- String lengths count code points (
mb_strlen). enum/const/uniqueItemscompare JSON values: maps key-order- insensitive, lists order-sensitive, scalars with===— so1and"1"differ, and1vs1.0differ although JSON has one number type.{}and[]decode identically in PHP and compare equal; the ambiguity is the same one the lenient matcher documents.
Consequences
- External callers of
Completionwhose schemas carry out-of-subset keywords (Service Interface:: complete Structured () $schemaand friends are fine — they are annotations) now get an immediate typed error instead of silent partial validation. Breaking, recorded in the CHANGELOG; there are no in-repo callers. - The ADR-105 input gate, the resume paths and the evaluation grader keep the lenient mode untouched. Migrating any of them to strict is a separate, deliberate decision with its own compatibility analysis — suspended runs persisted under lenient semantics must never start failing on resume.
- ADR-082's "no enum/pattern/oneOf" consequence is superseded by this ADR; its no-runtime-dependency decision and the repair round-trip stand.
- Provider-native structured output (OpenAI
json_schema, GeminiresponseSchema, Ollamaformat) was the separate follow-up ADR-082 named; ADR-128 delivered it, with the subset walker as its pre-flight gate.