Repository navigation
Step 40: JSON Schema — schema-compiled constrained decoding (#78) - #79
Conversation
Layer `SchemaTracker` struct on `JsonCharParser` to enforce JSON Schema constraints during generation. Schema compiled into flat `CompiledSchema` node array with property-name tries for efficient key masking. Zero-alloc Clone() via struct-copy (both parser and tracker are value types with InlineArray stacks). Supported: type, properties, required, additionalProperties, items, enum, const, $ref/$defs (non-recursive), anyOf, nested objects/arrays. - SchemaCompiler: System.Text.Json → CompiledSchema with $ref resolution - PropertyNameTrie: char trie for property name + enum value validation - SchemaTracker: ~1.3KB struct tracking schema position via InlineArray stacks - JsonSchemaConstraint: IDecodingConstraint with bounded mask cache (4096) - ResponseFormat.JsonSchema variant + TextGenerator dispatch - CLI --response-format json_schema --schema <json|@file> - 43 unit tests (compiler, trie, tracker, constraint integration) - Smoke test script: scripts/test_models_json_schema.py Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@codex please review this PR thoroughly - performance issues and possible improvements, potential bugs, code quality and architecture |
|
@gemini please review this PR thoroughly - performance issues and possible improvements, potential bugs, code quality and architecture |
|
Gemini ✦ This is a very solid implementation. Layering the schema semantics on top of the existing I have reviewed the PR and found one critical correctness issue regarding cache key collisions, along with a few performance improvement suggestions for the hot path. 1. Critical Bug: Cache Key Collisions in
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5a4975cfe1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ref readonly var node = ref GetNode(_currentNodeIndex); | ||
| if (node.EnumTrieIndex >= 0) | ||
| { |
There was a problem hiding this comment.
Enforce enum/const constraints for non-string values
StartValueString only activates value checking when EnumTrieIndex is present, which is built only for string enums/const. As a result, schemas like {"const": 1} or {"enum": [true, false]} are effectively treated as plain type constraints and can emit values outside the declared set. This breaks schema correctness for numeric/boolean/null/object enums and consts.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged — this is a known MVP limitation. Non-string enum/const values (e.g. {"const": 1}, {"enum": [true, false]}) are only type-restricted, not character-level constrained. Full enforcement requires character-sequence matching for literals and numbers, which is a fundamentally different mechanism from the string trie approach.
Added a TODO comment at StartValueString documenting this limitation for a follow-up.
| if (node.AnyOfNodeIndices != null) | ||
| { | ||
| types = JsonSchemaType.None; | ||
| foreach (int altIdx in node.AnyOfNodeIndices) | ||
| types |= GetNode(altIdx).AllowedTypes; |
There was a problem hiding this comment.
Preserve anyOf branch constraints after selecting a type
For anyOf, the tracker currently unions only AllowedTypes and never records which alternative is being followed. That means once a value starts, nested constraints from each branch (required keys, enums, object shapes, etc.) are not enforced, so outputs can satisfy neither branch while still being accepted by the mask.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Acknowledged — this is a known MVP limitation (same approach as Outlines and XGrammar). The current overapproximation (union of types from all branches) is safe: output is always valid JSON, but may not satisfy every branch perfectly.
Full branch narrowing requires maintaining parallel tracker states (one per viable branch), which is significant complexity. Added a TODO comment at IsValueStartCharAllowed documenting this for a follow-up.
| int bitPos = Array.IndexOf(propertyNames, reqName); | ||
| if (bitPos >= 0 && bitPos < 64) | ||
| requiredBitmask |= 1UL << bitPos; |
There was a problem hiding this comment.
Honor required keys that are absent from properties
Required entries are only encoded when their name exists in propertyNames; missing names are silently ignored. In JSON Schema, required still mandates presence even if the key has no schema in properties (especially when additionalProperties is true), so this allows objects to close while omitting required keys.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 5b4b179. SchemaCompiler now checks for required keys absent from properties and adds unconstrained schema nodes for them at compile time. The required bitmask and property name trie are rebuilt to include the added keys, so the constraint correctly blocks } until all required properties (even those without explicit schemas) have been emitted.
… keys (#78) - Fix cache key collisions: replace long hash with SchemaStateKey readonly record struct for collision-free mask cache lookup (Gemini critical) - PropertyNameTrie: replace Dictionary<char,int> children with sorted (char,int)[] flat array + linear search for L1 cache locality (Gemini perf) - SchemaCompiler: handle required keys absent from properties by adding unconstrained nodes at compile time (Codex P2) - Add TODO comments for known MVP limitations: non-string enum/const enforcement, anyOf branch narrowing, struct copying overhead Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Thanks for the thorough review. All items addressed in 5b4b179: 1. Cache key collisions (critical) — Fixed. Replaced 2. PropertyNameTrie children (perf) — Fixed. Replaced 3. Struct copying overhead (perf) — Added TODO comment on 4. Integer InNumberDot (minor) — No change needed. The |
Closes #78
Summary
JsonSchemaConstraintlayers aSchemaTrackerstruct on the existingJsonCharParserto enforce JSON Schema semantics during generation — type constraints, required properties, enum values, nested structuresSchemaCompilerconverts JSON Schema (System.Text.Json) into an immutableCompiledSchemaflat node array with property-name tries for efficient key masking. Resolves$ref/$defsat compile timeJsonCharParserandSchemaTrackerare value types withInlineArraystacks (~1.3KB + ~580B). Clone is a struct copy — zero heap allocations on the hot path--response-format json_schema --schema <json|@file>,ResponseFormat.JsonSchemarecord,TextGeneratorpattern-match dispatchtest_models_json_schema.pyfor cross-architecture validationTechnical Details
Architecture
The tracker observes structural events (object open, key complete, value start/end, array item, close) by comparing parser state transitions. At each decode step,
IsCharAllowedBySchema()intersects JSON-valid characters with schema-valid characters before the parser advances.Schema Features (MVP)
type(object/array/string/number/integer/boolean/null),properties,required,additionalProperties: false,items,enum,const,$ref/$defs(non-recursive),anyOf, nested objects/arraysKey Design Decisions
./e/Ein number states when schema type isintegerKey Files
src/DotLLM.Engine/Constraints/Schema/SchemaNode.cssrc/DotLLM.Engine/Constraints/Schema/SchemaCompiler.cssrc/DotLLM.Engine/Constraints/Schema/PropertyNameTrie.cssrc/DotLLM.Engine/Constraints/Schema/SchemaTracker.cssrc/DotLLM.Engine/Constraints/Schema/CompiledSchema.cssrc/DotLLM.Engine/Constraints/JsonSchemaConstraint.csIDecodingConstraintwith layered parser+trackersrc/DotLLM.Engine/Constraints/JsonCharParser.cssrc/DotLLM.Core/Configuration/ResponseFormat.csJsonSchemasealed record variantsrc/DotLLM.Engine/TextGenerator.cssrc/DotLLM.Cli/Commands/RunCommand.cs--schemaoption,json_schemaformatsrc/DotLLM.Cli/Commands/ChatCommand.cs--schemaoption,json_schemaformatscripts/test-schema.jsonscripts/test_models_json_schema.pyTest Plan
dotnet test --filter "FullyQualifiedName~Constraint"— 134 tests (9 existing + 43 new schema + 82 other)JsonConstraintTestspass unchanged (no regression)python scripts/test_models_json_schema.pyacross cached modelsdotnet run --project src/DotLLM.Cli -- run <model> --response-format json_schema --schema @scripts/test-schema.json -p "Output a JSON with name and age" -n 64 --temp 0🤖 Generated with Claude Code