ata-validator
1.61.3 · 29 Oct 2026MIT21.8k / weekRepositoryHomepagenpmAtom feed
1.61.3 is the newest release of 1.x.
In short
2 new6 fixed5 changed1 internal
Release note
ata has four code generation entry points, a closure compiler, an interpreter, a native walker and a set of buffer APIs. This release adds tests that run the official suite through each of them and fail on any split verdict. They found 157 disagreements between the code generators and 245 between the buffer APIs and validate(), 195 of those in the accepting direction. All are fixed or routed, the tests run in npm test, and the counts are held at zero.
Draft-07 is now read by its own rules, meta-schema references resolve from vendored copies, and the official suite stands at 1298 of 1299, 927 of 927 and 1133 of 1133, the same figures with code generation blocked. The interpreted engine is 4 to 14 times faster, and the linear-time pattern matcher runs a lazy DFA that sits within 1.2 to 3.5 times of V8's RegExp on typical patterns.
Fixed
The four code generation entry points disagreed with each other on 157 instances of the official suite. Most of it was the closure compiler, the boolean fallback behind
isValidObject()when the code generator declines a schema: it ignoredunevaluatedPropertiesandunevaluatedItemsoutright, treated a self-referencing$ref: "#"as always true, skippedadditionalPropertieswhenever nopropertiesmap sat next to it, and passed strings indate-time,time,urianddurationformat without checking them. Each of those accepted input it should have rejected. It now declines those schemas, so they reach an engine that handles them. The remaining disagreements were wrong rejections shared by both boolean paths:required,minPropertiesandmaxPropertiesapplied to non-objects,multipleOfhad no tolerance for fractional divisors,constandenumcompared objects by key order, anditemsstarted at index 0 whenprefixItemswas present.isValidObject()andvalidate()now agree on every suite instance.A
dependentSchemasbranch carryingadditionalProperties: falsehad that check hoisted out of its condition to the top level of the compiled function, so the restriction applied whether or not the triggering property was present.The error path counted a property matched by
patternPropertiesas additional underadditionalProperties: false. The same path now reports the real pointer (#/patternProperties/<pattern>/...) for a failing pattern subschema instead of the synthetic#/patternProperties, so source frames resolve for those errors.The buffer APIs (
isValid,isValidJSONabove the simdjson threshold,countValid,batchIsValid,isValidNDJSON,isValidParallel,isValidPrepadded) disagreed withvalidate()on 245 of 2222 suite cases, and in 195 of those they accepted a documentvalidate()rejects. The native walker behind them does not handlecontains,unevaluated*,dependencies,dependentSchemas,dependentRequired,propertyNames,patternProperties, tuple-formitemsandprefixItems, cross-document$ref, embedded$id, an emptyenum, a boolean root schema, Unicode property escapes inpattern, or thehostname,date-time,time,uri-referenceanddurationformats. For a schema using any of those, every buffer API now parses the bytes and answers throughvalidate();lib/buffer-gate.jsholds the list. Schemas without them keep the zero-copy path.tests/test_buffer_path_parity.jsnow compares all three dialects, 3359 cases, and holds the disagreement count at zero.A value failing a
patternPropertiessubschema was reported at runtime as a generic "value invalid for key" error on the parent object with the synthetic pointer#/patternProperties. The subschema is now generated in place, so the error comes from its own keyword, at the key's path (/x-flag), with the real pointer (#/patternProperties/^x-/type), and source frames resolve for it. Patterns containing/or'produce a correctly escaped pointer.tests/test_codegen_entrypoint_agreement.jsdrives the whole suite through each entry point directly and fails on any split verdict. It runs as part ofnpm test.
Added
validator.engine()reports which engine answersvalidate()for the schema:'codegen','closure','native'or'interpreter'. A diagnostic for startup logs and benchmarks. Measured over fourteen request-shaped schemas (body, params, query, shared$ref,oneOf,if/then, local$defs), thirteen take the generated path;patternPropertieswithadditionalProperties: falseis the one that goes to the interpreter.formatMode: 'inject'fortoStandaloneModule,bundleStandalone,bundleCompactandbuild(). The output carries no custom format source; it exportssetFormats(map)and looks each format up from that registry at validation time, with a named error if one is missing. This is for formats that cannot be serialized: functions that close over variables, bound functions, or code rewritten by coverage and transpile steps. The default'embed'is unchanged in behavior, but it now checks each function's source at build time and throws with the format's name when it would not survive embedding, instead of emitting a module that fails on first use.
Changed
Draft-07 is now read by its own rules rather than as 2020-12 with renamed keywords. A schema object carrying
$refis that reference and nothing else, so sibling keywords,$idincluded, are ignored, as the draft specifies. A fragment-only$id("$id": "#name") is a plain-name anchor. A document supplied throughschemasoraddSchema()that declares no$schemaof its own is read under the root's draft. JSON Pointers written against array-formitemsstill resolve after the keyword is normalized toprefixItems. Schemas that declare no$schemaare unaffected. Draft 7 on the official suite goes from 916 to 927 of 927.The code generator now declines, and the interpreted engine answers, whenever a document reachable through a cross-document
$refuses$dynamicRef,$dynamicAnchor,unevaluatedProperties,unevaluatedItems, or an embedded$id. Before, the generator emitted a vacuous check for such a reference and accepted everything behind it:{ "$ref": "https://json-schema.org/draft/2020-12/schema" }accepted{ "type": 1 }. These checks now live in one function,sharedCodegenGate, that every entry point runs first. Draft 2020-12 goes from 1294 to 1298 of 1299 and the v1 dialect from 1131 to 1133 of 1133; the one remaining 2020-12 miss needs$vocabulary.The interpreted engine extends the dynamic scope whenever evaluation enters a schema resource, not only when it lands on the resource's root. A
$dynamicRefreached throughfirst#/$defs/stuffnow sees resourcefirstin scope, which closes the last$dynamicRefcase the interpreter missed. With code generation blocked the figures are the same as with it: 1298 of 1299, 927 of 927, 1133 of 1133.The interpreted engine is between 4 and 14 times faster depending on the schema. Each schema node is resolved once into a fixed-shape plan (type bitmask, compiled pattern, looked-up format, one flag per keyword group) and children are linked at plan time, so the walk reads no schema properties and does no map lookups. On a six-field object schema a warm
validate()went from 3786 ns to 343 ns; a$ref-heavy schema from 5354 ns to 366 ns; a recursive$dynamicReftree from 5302 ns to 526 ns. The compiled path is unchanged at about 44 ns on the same schema.The linear-time pattern matcher now runs a lazily built DFA over the Thompson NFA, with cached ASCII transitions and a fallback to the NFA walk past 256 states. On typical anchored patterns it is within 1.2 to 3.5 times of V8's backtracking engine (
^[0-9]{5}$14 ns against 12 ns, an email pattern 70 ns against 21 ns) while keeping the linear bound:^(a+)+$against 100,000 characters takes 1 ms. This matcher backspattern,patternPropertiesandpropertyNamesin every engine and in standalone output, so all of them gain.