ata-validator
1.61.3 · 29 Oct 2026MIT21.8k / weekRepositoryHomepagenpmAtom feed
Upgrading ata-validator from 1.19.0 to 1.61.3
65 releases in betweennotes for 35
Reading releases, release notes and advisories…
- 1+
- change to make
- 0
- advisories fixed
- 36+
- new
- 58+
- fixes
Counted from 35 of 65 release notes. Advisories are counted from the advisory database, in full.
Before you upgrade
Tick each one off as you go- breaking1.29.0
Security
- 1.58.0
An object in
enumorconstcould be matched by a different object. The native engine compared objects through a canonical text that put each member name between quotes without escaping it, so{"a\":1,\"b": 2}, an object with one member, read the same as{"a": 1, "b": 2}and passedenum: [{ "a": 1, "b": 2 }]. Member names are now escaped as JSON strings, which makes the text one-to-one. The same text decideduniqueItems, where it made two different objects count as equal and rejected the array. - 1.58.0
A violation deep enough in the document was accepted. Past a depth limit (100 schema frames, which a recursive schema reaches about 50 levels into the data) the native walker stopped and took the rest of the document as valid. It now rejects there, with the error code
depth_exceededin the C++ API, and the JavaScript APIs above send a schema that refers back into itself, or that is nested more than 40 levels, to the JavaScript engine, which has no such limit and answers asvalidate()does at any depth. That route was faster for such schemas, not slower: 4,638 to 2,495 ns forisValidon a 20-level document. - 1.58.0
isValidParallelandcountValidread up to 64 bytes past the end of the caller's buffer for a line ending near it, andisValidPrepadded(buffer, length)read as many bytes aslengthsaid whatever the buffer's size. A line without 64 bytes of the buffer after it is now copied to a padded buffer first, andisValidPrepaddedreturns false for a length past the buffer and copies when the padding is short. The read did not fault, since the engine checks for a page boundary, and the bytes past the end did not decide the answer. - 1.58.0
isValidParallelcould return false for a valid line: its workers wrote their answers into astd::vector<bool>, which packs eight lines to a byte, so neighbouring lines raced. One byte per line now; 20 runs of 200 lines all agree withvalidate(). - 1.45.0
With code generation blocked, validating and reading the errors takes 4.7 µs per tsconfig.json sample where it took 8.5, and 427 µs per SARIF log where it took 568; the other schemas measured are unchanged;
- 1.45.0
On the valid samples of five popular schemas, a verdict takes 9.9 µs where it took 16.2, and a validation that collects errors 16.0 µs where it took 23.4.
- 1.27.1
Security patch.
Release by release
1.61.1 – 1.61.33 releases, no release note
- new
A schema with a
defaultanywhere (ata fills defaults without being asked, so it has a preprocess pass) now builds its rich errors where they fail, as other schemas do: the path was closed to schemas with a preprocess pass, though it reads the document validate() has already processed. Onerequirederror on a four-field user with a defaultednewsletterwent from 313 to 248 ns with its error list read; only the one-pass swap, which replaces validate() itself, stays off for such schemas. Found through the fastest-validator comparison in ata-core/validator-benchmarks (issue #74). - new
The Node entry warms the engine once the event loop is free: where a validator was built and is still untouched by then (not compiled, no method read off it), one check of a small schema runs, accepted and rejected, so that V8 compiles the generator's functions and the error builders before the first request instead of on it. On a four-field schema with the generator already loaded, a server's first verdict went from 3.1 to 3.4 ms to 0.5 to 0.6, the first error list from 6.7 ms to 0.8, at 1.5 to 2 ms of CPU once. A script that builds and checks in the same tick never runs it, the timer is unref'd, and
ATA_NO_WARM=1turns it off. The browser build has done the same since 1.57.0 (lib/tiering.js); the cold-start benchmarks, which check in the same tick as they build, are unchanged by it.tests/test_node_warm.jsholds the conditions. - new
ata-validator/buildexportsbunTarget(the schema as a TypeScript type and the.bun.tsmoduleata build --target bunwrites; withtags: truethe split into a type with Bun'sSchematags and a residual schema, made for the typed validator the compiler's pull request carried until 2026-10-25) andverify(verifyOne,verifySource,generate), for a build plugin that replacesnew Validator(schema)inside aBun.build({ compile: { aot: true } })and holds what it wrote against the runtime. - new
ata verify <glob>holds each schema's compiled module against a runtimeValidatorbuilt from the schema now: documents made from the schema (--generate, 200 by default, half with one rule broken), the project's sample documents (--samples, JSON or JSON Lines files or a directory), verdicts and error lists compared, a module whose schema hash no longer matches the schema reported stale, a non-zero exit on any disagreement, stale or missing module,--jsonfor machines. The check behind the fixes of 1.59.0 and 1.60.0, as a command a project runs in CI.
- new
- new
A validator the build compiled away (
fromCompiledaround a module written ahead of time, what@ata-project/unpluginputs in place ofnew Validator(schema)) carries Standard Schema V1 as the runtime Validator does: the same~standardobject, built bylib/standard-schema.jsfor both, with the same issues, value and published schema. A compiled validator can now be handed to a form library, a router or an RPC layer that reads~standard.tests/test_compiled_standard.jscompares the two. - fixed
toStandaloneModule(schema, { coerceTypes })andtoStandaloneModule(validator)for a validator built withcoerceTypesorremoveAdditionalwrote a module that checked the data as it was, so it rejected{ id: "5" }where the validator accepts it. The module now runs the validator's pass over the data before the check, asbundleStandalonedoes since 1.59.0, and refuses where that pass cannot be written into a module.fastify-ata/standalonebuilds its files through this call.
- new
- new
bundleStandalone(schemas, { order: true })hands each validator's errors out in the runtime's order, astoStandaloneModuledoes with the same option. rjsf's precompiled validator writes its module withbundleStandalone, and its error list showed anadditionalPropertieserror before thepropertieserrors where the runtime validator shows it after.tests/test_aot_order_defaults.jsholds 553 bundled groups of the 2020-12 and draft-07 suites to the runtime's errors, in order. - new
Each validator of a bundle whose schema has a default carries
applyDefaults(data), the runtime's defaults pass, as a property, as standalone modules export it. rjsf's precompiled validator can call it to match its runtime validator.tests/test_bundle_preprocess.jscompares 8400 generated documents with the runtime: the data afterwards, the verdict and the errors in order. - fixed
bundleStandaloneleftcoerceTypesandremoveAdditionalout without a word, so a bundle built withcoerceTypes: truerejected{ age: "5" }, which the runtime accepts. Each bundled validator now runs the runtime's pass over the data for those options before it checks, and withuseDefaults: truefills defaults the same way. Where the generated pass cannot carry what the runtime would do (coercing the items of an array at the root, for one),bundleStandalonethrows instead of writing a module that answers differently. Without these options a bundle checks the data as it is, as before.
- new
1.56.0 – 1.58.14 releases, nothing new or fixed; 1 without a note
- fixed
bundleStandaloneandbundleCompactmodules threwReferenceError: _cpLen is not definedon a string whoseminLengthormaxLengthneeded counting code points: a one-character value underminLength: 1, an emoji under amaxLength. The length checks call a code-point counter thattoStandaloneModuleembeds and the two bundles did not. This goes back to 1.10.0 at least, and reached forms built on rjsf's precompiled validator, which usesbundleStandalone: typing one character into a required name field threw. The bundles now embed the counter when their code calls it;tests/test_bundle_cplen.jsruns both bundles in both formats.
- fixed
- fixed
The
uri,uri-reference,iriandiri-referenceformats follow RFC 3986 in full. They checked the characters and, foruri, the shape of the authority; a bracketed host was not read, sohttp://[::ffff:01.2.3.4]passed, anduri-referencechecked only the characters, so//example.com:abc/p,//a@b@example.com/,/[::1]and1:bpassed. One walk now parses scheme, authority (userinfo, a host that is a name, an IPv6 literal with its embedded IPv4, an RFC 6874 zone id or an IPvFuture, and a digits-only port), path, query and fragment, and rejects brackets outside the host, a colon in the first segment of a relative path and a second#. It lives inlib/uri-core.jsand is the same text in the interpreted engine, generated code and standalone modules. Theuriexpression in front of it now takes a host with a port, which most of SchemaStore's misses were (http://localhost:5000, 73 to 34 ns in generated code), and a reference made only of plain characters, a file name or a path, is answered by one scan before the walk, so those checks cost what the character scan did. On the 26321 strings in SchemaStore's sample documentsuriandirigive the same answers as before;uri-referencenow rejects 346 of them, every one a string that is not a URI reference ([string],00:00:01,2022-01-19T22:02:33Z), 277 of which the default validator still accepts. - fixed
uri-template: a%in a literal or a variable name must start a percent-encoded triplet (a%,a%4,a%GG,{%}were accepted). - fixed
idn-email: a domain label that starts or ends with a hyphen is refused, asemailrefuses it (the official suite's 7de0e6a adds these cases). - fixed
email: the inside of a quoted local part is checked ("test\©"@iana.orgwas accepted, a quoted pair takes one printable ASCII character), and theIPv6:tag of an address literal is read in any case (a@[ipv6:::1]was rejected). - fixed
A node without
type: objectchecked an array against itsproperties:[1, 2]was rejected by{ properties: { length: { maximum: 1 } } }, through the array's ownlength, while the interpreter and the default validator accept it. Generated code now skips arrays there when a property name is one an array can own (lengthor an index); other names were already safe, since an array only inherits them. A required key on such a node is also read before it is asked whether it is the object's own, which saves a test.tests/test_untyped_properties.jscovers both on 189 documents. - fixed
Draft-04 is read as draft-04. A schema that declared it was read as 2020-12:
iddid not set the base URI, tupleitems,additionalItemsanddependencieswere not applied, andconst,containsandpropertyNames, which draft-04 does not have, were. It now goes through the draft-06 rules plus its own (idfor$id, those three keywords dropped), and its meta-schema is vendored with the others. The official suite's draft-04 tests pass, 618 of 618, also with code generation blocked;npm run test:suiteruns them. SchemaStore's draft-04 schemas answer their sample documents as before. - fixed
A declared property holding
undefinedstill got different answers in 1.53.0: the generated code checked the value where the key was required, where the node had notype, and insideif,notand similar subschemas, and the engines disagreed on whether a required key holdingundefinedwas there. Every engine now reads such a key as the default validator does, and as it reads once serialized: absent. Its value is not checked, and a required key holding it is missing, so{ a: undefined }no longer passesrequired: ['a']on any engine.{ if: { properties: { t: { const: 'x' } } }, then: false }accepted{ t: undefined }on generated code and rejected it on the interpreter; both reject it now.tests/test_undefined_property.jscovers 21 shapes on every engine, with and without code generation.
- fixed
- new
Custom keywords can ask for their data's context with
dataContext: true: the per-value function is also given{ instancePath, parentData, parentDataProperty, rootData }, as the default validator's keywords are, so it can read or remove the value where it sits. A schema using one runs on the interpreted engine's collecting walk, which knows where every value is; other schemas are untouched.ata-validator/compatgives every keyword this context andit.schemaPath, which is what express-openapi-validator'sreadOnlyandwriteOnlykeywords use to strip read-only properties from a request. - new
ata-validator/compattakes what a library sets up on it in its own code: the formats plugin'saddFormats()is accepted and registers its formats as the reference does, a keyword registered under a list of names is registered under each, and a draft-04 schema (OpenAPI 3.0's meta-schema) is read as draft-07 with itsidas$idand checked against draft-07's meta-schema after its boolean bounds are normalized. A keyword defined only as generated code is now refused when a schema uses it rather than when it is registered, so a library that registers such a keyword for an option it may not use can take the class. With these, express-openapi-validator runs oncompatwith one change of its own (a portablex-eov-type) and validates requests against GitHub's and Stripe's API descriptions, which the default validator cannot load. - new
ata-validator/compatdoes what the reference does while it validates, for the schemas that need it.coerceTypesconverts a value where eachtypeis checked, so it reaches values behindanyOf,oneOf,allOfand$ref;useDefaultsfills defaults behindallOfand$ref; thediscriminatoroption applies theoneOfbranch the tag names; a schema registered under a fragment-only key such as#/components/schemas/Petis found by a$refof that string; a format registered withtype: 'number'is checked on numbers; and custom keywords takebeforeandpostto order keywords that change the data. Errors name the schema a$refpoints to, andoneOfandanyOfwith no passing branch use the reference's wording. express-openapi-validator's own suite, run with ata injected through a small factory option, passes 470 of 470, as it does on the default validator; it passed 429 before these. On 2400 generated documents with composition andcoerceTypes: true, verdicts and coerced data match the reference in every case. A schema keeps its compat copy across compiles, so the work done once per shared schema is not done again: with express-openapi-validator on Stripe's API description, the first request on a second route went from 280 to 200 ms, and the first request overall stays at 1.1 s on both Stripe's and GitHub's. - fixed
A declared property whose value is
undefinedgave different answers on different engines: the generated code skipped it, as the default validator does, and the interpreted engine and the closures used where code generation is blocked checked it and rejected. All of them now skip it. No JSON document carries such a value, but an object about to be serialized often does ({ id: req.params.id }with no such parameter).tests/test_undefined_property.jsholds the engines together.
- new
135 other lines we could not sort, mostly version bumps
- 1.61.0
The standalone module's error collector (the default form, with schema-source frames) reads each property once into a local, names a closed object's declared keys outright instead of a Set lookup per key when there are at most eight, and reuses the node's plain-object flag in that walk instead of reading the prototype again. A rejected ten-item order with its error list built went from 378 to 244 ns on Node (the one-pass form, 102, is unchanged and still opt-in for its size); the module's size does not move. Found by profiling line by line: one closedness walk held 31% of the samples.
- 1.61.0
ata build --target bunwrites the one-pass error function by default and no schema-source frames, as an executable compiled ahead of time pays no bundle size: a rejected ten-item order with the error list built went from 724 to 189 ns under Bun's compiler (on Node, 378 to 105 against 99 for the runtime). The default for every other build stays the older collector with frames, since the one-pass form is 20% larger gzipped over 60 SchemaStore schemas (53 of them larger by more than 5%, the worst 1.6x);--one-passand--no-sourcechoose it by hand. The plugin's ahead-of-time path does the same. - 1.61.0
Generated code walks and counts a closed object's keys in the form of the engine it is written for: for...in on V8, where it is the cheaper form, an indexed loop over
Object.keyson JavaScriptCore, where for...in is a slow path. The runtime writes for the engine it runs on;ata build --target bunand the plugin's Bun entry write for JavaScriptCore. One form is written, so neither engine pays a branch per object (an earlier form with a load-time flag cost V8 5 to 7% on small schemas). A third form,jsc-aot, is what--target bunand the plugin's ahead-of-time path write: a closed object whose declared keys are all required is checked by readingObject.keysonce and comparing it with the declared list in order, the orderJSON.parsegives, with a membership walk when the order differs. Under Bun's compiler that put the plain module level with Bun's ownvalidate<T>on a ten-item order (105 to 107 ns against 96 to 105, from 108 to 112); on Bun's JIT the same form loses 5 to 15 ns, which is why it is its own target. The prototype read picks__proto__on V8 andObject.getPrototypeOfon JavaScriptCore at generation. Measured on a ten-item order: Bun 1.4.4's JIT 65 to 53 ns a document, Bun's ahead-of-time compiler (pull request build) 227 to 108, Node 25 unchanged. - 1.59.0
Requiring the package no longer loads the code generator. It loads when a validator first compiles, or once the event loop is free, so a server reaches its first request without waiting for it. Importing the package went from 4.55 to 1.64 ms (Node 25, the median of 21 interleaved runs), and a server building 20 validators at startup from 4.71 to 1.83 ms. Validators still compile at their first call, and a script that validates as soon as it starts takes as long as before.
benchmark/bench_fastify_boot.mjsnow counts loading the library on both sides, as "from a cold process" says; the README's Fastify figure moves from 18.3 against 2.67 ms (loading left out) to 30.3 against 9.7 ms. - 1.58.0
defaultvalues are filled in every schema applied to a value, as the default validator'suseDefaultsfills them: behind a$refinto the same document (recursive ones included), in eachallOfmember, in array elements underitemsandprefixItems, underadditionalPropertiesandpatternPropertiesfor the keys they cover, and underdependentSchemasfor the keys present. The pass stopped atproperties, so arequiredproperty whose default sat behind a reference or in an array element was rejected where the default validator accepts it.anyOf,oneOfandnotfill nothing, as there.if/then/elsefill nothing here either, since which branch applies is only known once the value has been checked; the default validator fills some of those cases. On SchemaStore's sample documents for the 372 schemas with defaults it compiles (1,287 documents), the filled data differs from the default validator's on 161 documents, down from 488, and the verdict on 23, down from 42; every remaining difference is in a schema withoneOf,anyOf,ifornot. All three passes fill the same: the generated one, the closure one (the interpreted engine, and where the generated pass declines a schema), and a module'sapplyDefaults, which now embeds the closure pass where the generated one declines: 405 of 878 SchemaStore modules exportapplyDefaults, up from 289.tests/test_defaults_reach.jscovers each place, against values taken from the default validator, and 300 generated schemas on all three passes. - 1.58.0
This can reject a document that was accepted before, when a schema's own default breaks one of its rules. Of SchemaStore's 1,537 positive samples, 17 are now rejected; the default validator with
useDefaultsrejects 16 of them. The 17th fillslayout: truewhere a siblingthensaysnot: { required: ["layout"] }: the default validator checksallOfbefore it fillsproperties, ata fills first.useDefaults: falseleaves the input as it is. - 1.58.0
Inside a value the pass made from a default, a schema already applied to an enclosing value is not applied again. A schema whose object default refers back to an enclosing one would otherwise fill without end; Renovate's schema did, and the default validator throws a
RangeErroron it. An empty Renovate configuration fills to 22,774 bytes, as before. - 1.58.0
A validator whose schema has defaults, or that coerces, builds its errors when they are read, as other validators do. It built them on every rejection, read or not: on Kestra's schema that was 25 ms of the first
validate()spent ordering errors nobody asked for. A rejection on a small schema with defaults went from 46 to 22 ns when the errors are not read. - 1.58.0
A schema whose defaults would expand past the generated pass's size limit is handed to the closure pass before any code is written. SARIF's schema wrote 4,000 lines in 2.7 ms and threw them away. The generated source is unchanged on all 1,692 SchemaStore cases checked.
- 1.58.0
In the browser build, the plan compiler, which turns a schema the interpreted engine checks often into a tree of closures, is no longer in the page's first load. It arrives through a dynamic import, a chunk of its own in a bundler that splits, once the page is idle after its first validator (
lib/interpreter.browser.js, which the package.json browser field puts in place oflib/interpreter.js). Until then the plain walk answers, as it already did for each schema's first thousand calls, with the same answers and the same depth of data before the stack runs out. The first load is 53,953 bytes gzipped, from 58,245 in 1.57.0. In Chrome with the CPU slowed 4x and a 1.6 Mbps link, a react-hook-form page withataResolverwas ready in 1,594 ms instead of 1,672 (1,668 for a second copy of 1.57.0); the first submit 300 ms after the form was built went from 2.50 to 2.25 ms, and a submit before the page was idle did not move (17.43 and 16.86 before, 16.92 after). With code generation blocked, a hot validator answers as before once the chunk is in (49.5 ns a rejection, both).tests/test_browser_plan_lazy.jscovers the split and the answers before and after.
9 internal lines (docs, tests, tooling) left out.