JSON Schema Support - ChristianPresley/zig-sdk GitHub Wiki
Tool input and output schemas are JSON Schema 2020-121. The SDK validates tool arguments and structured output with its own validator in src/mcp/schema/validator.zig. The validator evaluates all keywords of the 2020-12 vocabularies for the core, the applicators, the unevaluated locations and the validation. It has its own regular expression engine for pattern and patternProperties. The draft 2020-12 files of the JSON Schema Test Suite check the validator.
Applies to: JSON Schema 2020-12, zig-sdk 0.1.0. The regular expression engine, the unevaluated locations, the dynamic references, $id in subschemas, pattern in derived schemas and the test suite: zig-sdk 0.2.0.
- Types:
typewith one name or a list.integeraccepts1.0. - Values:
enum,const. - Objects:
properties,patternProperties,required,additionalProperties,unevaluatedProperties,propertyNames,minProperties,maxProperties,dependentRequired,dependentSchemas. - Arrays:
items,prefixItems,unevaluatedItems,contains,minContains,maxContains,minItems,maxItems,uniqueItems. - Numbers:
minimum,maximum,exclusiveMinimum,exclusiveMaximum,multipleOf. - Strings:
minLength,maxLength,pattern. The length counts code points. - Logic:
allOf,anyOf,oneOf,not,if,then,else. - References and identifiers:
$ref,$dynamicRef,$defs,$anchor,$dynamicAnchor,$idat the root and in subschemas, and$schemaat the root of a schema resource. Boolean schemas.
A property name that properties or patternProperties matches is not an additional property1. propertyNames can contain a pattern. A count keyword, such as maxLength or minItems, accepts a number with a zero fraction, such as 2.0.
unevaluatedProperties and unevaluatedItems use the annotations of the subschemas that the instance passes1. Thus they see the properties and the items that the applicators and the references evaluated. The keyword not gives no annotations. The sets of evaluated locations also use evaluation budget, see Limits.
The keywords of the vocabularies for the format, the content and the meta-data are annotations2. The validator keeps them and does not evaluate them. This is the default behavior of 2020-12. Thus format, contentEncoding, contentMediaType and contentSchema never make an instance invalid. The compiler checks only the shape of their values: contentEncoding and contentMediaType are strings, and contentSchema is a schema.
title, description, default, examples and x-* keys are annotations too. The validator ignores unknown keywords. The keywords $recursiveRef and $recursiveAnchor of draft 2019-09 are unknown keywords in 2020-12, so the validator ignores them.
A document can hold more than one schema resource. The root and each subschema with $id start a resource. The compiler resolves $id, $ref and $dynamicRef against the base URI of their resource with the rules of RFC 39866. The base URI of a root without $id is validator.default_base_uri, which is https://schema.invalid/root.json. The domain .invalid never resolves.
The compiler resolves each reference one time, at registration. It also compiles the target of each reference. Thus a reference cannot bring an unchecked subschema to the validator. A reference to the $id of a subschema in the same document is not a remote reference.
$dynamicRef uses the dynamic scope1. The dynamic scope is the list of the schema resources that the evaluation entered.
The engine in src/mcp/schema/regex.zig reads the ECMA-262 dialect4 that JSON Schema recommends2. The server compiles each expression one time, when it registers the tool. The compiled schema keeps the program.
The engine compiles a pattern to a Thompson automaton and runs it as a Pike machine5. It never goes back in the input. The time of one match is linear in the input length multiplied by the program size. Thus a pattern such as (a*)*b cannot cause catastrophic run time.
A search has no implicit anchor, so "pattern": "b" matches "abc". Use ^ and $ to match the full string. The engine has no flags. ^ and $ match only at the start and at the end of the string.
The engine reads the pattern and the input as Unicode code points. It uses the rules of the ECMA-262 u flag for escapes. The engine supports these features:
- Literals and the escapes
\t,\n,\r,\f,\v,\0,\cX,\xHH,\uHHHH,\u{H...}and surrogate pairs. - The dot. It does not match a line terminator.
- Classes with ranges and negation, and the class escapes
\d,\D,\w,\W,\s,\S.\sincludes the Unicode space separators. - The assertions
^,$,\b,\B. - Groups, named groups, non-capture groups and alternation.
- The quantifiers
*,+,?,{n},{n,},{n,m}and their lazy forms. A lazy quantifier gives the same result as a greedy one, because the engine only tells if a match exists. - The property escapes
\p{Any},\p{ASCII}and\p{ASCII_Hex_Digit}, and their\Pforms.
The engine accepts four forms from ECMA-262 Annex B4. A { that does not start a quantifier is a literal. A } or a ] outside a class is a literal. A - next to a class escape in a class is a literal, as in [\w-.]. An identity escape can escape each code point that is not an ASCII letter or digit.
The server rejects a tool at registration with error.UnsupportedKeyword when a pattern uses one of these features:
- Backreferences:
\1to\9and\k<name>. - Lookahead and lookbehind:
(?=,(?!,(?<=,(?<!. - Pattern modifiers such as
(?i:x). - Legacy octal escapes such as
\01. - Other Unicode property escapes such as
\p{Letter}or\p{Script=Greek}.
The validator itself gives error.UnsupportedRegex for these features. The server changes this error to error.UnsupportedKeyword.
Server.Options.allow_unsupported_schema_keywords accepts them. Then the validator ignores that pattern, and a patternProperties entry with that expression matches no property name. The option has no other effect. The validator option allow_unsupported_keywords does the same.
The server always rejects a pattern that is not a valid ECMA-262 expression, or that is larger than the limits. The validator gives error.InvalidRegex or error.RegexTooLarge, and addTool then returns error.InvalidSchema.
The option allow_unsupported_schema_keywords does not change these rules:
- Remote references. The compiler rejects a
$refor a$dynamicRefto another document witherror.RemoteRef. The SDK never fetches a schema from the network3. - Custom meta-schemas. The compiler rejects a
$schemavalue other than the 2020-12 dialect witherror.UnsupportedDialect. MCP requires an error for a dialect that the SDK does not support7. -
$schemain a subschema that is not the root of a schema resource. The server rejects the tool witherror.UnsupportedKeyword.
The regular expression features in the list above are the only other limit.
The test JSON Schema Test Suite draft 2020-12 in src/mcp/schema/suite_test.zig runs the draft 2020-12 files of the JSON Schema Test Suite. The files are in test/fixtures/json_schema_test_suite/, with the license and the upstream commit. The test runs the files of the required tests and eight optional files. The optional files test anchors, dynamic references, ECMA-262 regular expressions, $id, schemas without $schema and unknown keywords.
Some schemas of the suite refer to a remote document of the suite. The validator never gets a schema from the network. Thus the test puts the remote documents into $defs of the root, each with its URI in $id. The core specification calls this a bundle1.
On 2026-09-30, 1378 cases passed and no case failed. 38 of the passed cases used a bundle. The test skipped 34 cases. The table skipped in the test gives the reason for each skipped group:
- The schema refers to the 2020-12 meta-schema. The validator does not get the meta-schema from the network.
- A remote document has an
$idthat is not its retrieval URI. A bundle cannot keep both URIs. - The schema uses a custom meta-schema. That is a dialect other than 2020-12, and MCP requires an error for an unsupported dialect7.
- The regular expression uses a Unicode property escape that the engine does not support.
| Limit | Default | On overflow |
|---|---|---|
max_depth |
32 | The compiler rejects the schema, or the instance is invalid. |
max_subschemas |
4096 | The compiler rejects the schema. |
max_ref_hops |
64 | The instance is invalid. |
max_errors |
32 | The report stops at the limit. |
eval_budget |
100 000 | The instance is invalid. |
max_pattern_bytes |
4096 | The compiler rejects the schema. |
max_regex_states |
4096 | The compiler rejects the schema. |
The limits are the fields of limits.schema in the server and the client options.
eval_budget counts the subschema evaluations. The sets of evaluated locations for unevaluatedProperties and unevaluatedItems also use one unit for each 64 locations. max_regex_states counts the instructions of one compiled expression. A counted repetition multiplies the size of its operand. The engine also rejects groups in groups deeper than 128 levels.
Server.addTool derives the input schema from the argument struct at compile time. The schema does not require optional fields and fields with a default value. The derived schema sets additionalProperties: false.
A pub const json_schema declaration can give a description for the struct. Its fields give description, header and pattern per field. The header value becomes the x-mcp-header annotation. Only a string field can have a pattern, else the compilation fails.
const Args = struct {
code: []const u8,
pub const json_schema = .{
.fields = .{ .code = .{ .description = "Currency code", .pattern = "^[A-Z]{3}$" } },
};
};- JSONSCHEMA-CORE-2020-12: "JSON Schema: A Media Type for Describing JSON Documents".
- JSONSCHEMA-VALIDATION-2020-12: JSON Schema Validation.
- MCP-TOOLS-2026-07-28: Tools, the rules for input schemas.
- ECMA-262-REGEXP: ECMAScript Language Specification, RegExp objects and Annex B.
- COX-REGEXP-VM: "Regular Expression Matching: the Virtual Machine Approach".
- RFC3986: Uniform Resource Identifier (URI): Generic Syntax, the resolution of references.
- MCP-BASIC-2026-07-28: Base protocol, the rules for the JSON Schema dialect.