ata-validator
1.61.3 · 29 Oct 2026MIT21.8k / weekRepositoryHomepagenpmAtom feed
Upgrading ata-validator from 0.17.5 to 1.61.3
113 releases in betweennotes for 101
- 2+
- changes to make
- 0
- advisories fixed
- 78+
- new
- 140+
- fixes
Counted from 101 of 113 release notes. Advisories are counted from the advisory database, in full.
Start here
Before you upgrade
Tick each one off as you go- breaking1.29.0
- breaking1.0.0
Security
- 1.58.1
A string in
enumthat needs escaping in JSON could be matched by a different string through the native engine. Its fast path for string enums compared the value's bytes with the entry's JSON text between the quotes, so forenum: ['a"b']the valuea\"b, with a backslash, matched. The fast path now takes only entries with no escape in them; the others are compared as canonical text, as every other path does. This reaches the same APIs as the native fixes in 1.58.0 (isValid,isValidJSONandvalidateJSONon 8 KB or more, the NDJSON and batch methods);validate()andisValidObject()were not affected.tests/test_native_security.jsandtests/test.cppcover it. - 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.40.1
A property name in a schema could run as code in the validator ata generates for it. Where
requiredordependentRequiredsits underitems,patternProperties,additionalPropertiesor a$ref, and in every standalone module, the error for a missing property put its name in a single-quoted string literal escaped only for double quotes, so a name holding a single quote closed the literal and the rest of the name was compiled into the validator. It ran when a document missing that property was validated. Only a schema written to do this is affected, so an application that compiles schemas it does not control, at run time or withata build, should upgrade; one whose schemas are its own is not exposed. Affected: 0.4.3 to 1.40.0. The helper that escapes these strings now escapes the single quote too; error messages andparamsare unchanged.tests/test_schema_code_injection.jsputs strings built to break out of a literal into every string a schema can carry, runs each schema through every path that generates code (the runtime compiler, the interpreter, standalone modules, bundles, the compiled wrapper,parse()), validates documents carrying the same strings and renders the errors, and checks that none of it ran. - 1.27.1
Security patch.
Release by release
- new
ata diff <old> <new>: what a change to a schema does to documents. The documentsata verifymakes, from both schemas, plus the project's samples, are checked by aValidatorbuilt from each schema; the ones the old schema accepts and the new one rejects, and the reverse, are reported grouped by the rule that decides them, with one example each. Exit code 1 when the new schema rejects what the old one accepted, so a pull request that tightens a schema fails the check until someone looks.lib/schema-diff.js,tests/test_schema_diff.js. - new
ata-validator/compiled-standard:bareStandard(mod, schema), the compiled-module wrapper for a validator that is only ever handed to a Standard Schema consumer (a router's validator middleware, a form library, an RPC layer) and never called by its own code. Such a consumer reads~standardalone, and an issue is a message and a path, so the wrapper is built on a module written withplainErrors: trueand carries no error pipeline: 733 bytes gzipped, standing alone as an ES module. The issues, the input after defaults and the Standard JSON Schema document are the runtime's on every suite case the compiler takes (tests/test_compiled_standard.js).@ata-project/unpluginuses it where it sees that handover, through Hono'ssValidator,hono-openapi, TanStack Form, oRPC, tRPC's.input()and Elysia's hooks: a Hono route bundled for Cloudflare Workers goes from 21.0 to 12.2 KB gzipped against 7.7 KB for Hono alone, an Elysia route from 120.1 to 110.1 against 106.8, a tRPC procedure from 18.0 to 7.8 against 4.6.
- new
- fixed
Under Bun's ahead-of-time compiler a runtime
Validatortook generated code again, which the compiler interprets: 487 ns a document against 78 on the interpreted engine. ata told the two apart byBun.isAOT, which the compiler's pull request has since removed in favour of an"aot"export condition. The package now leads its exports with that condition, namingindex.aot.jsandindex.aot.mjs, which mark the process before the package loads (lib/aot-mark.js), and lists them as side effects so a bundler keeps them. Measured on the pull request's build of 2026-10-27;tests/test_aot_entry.jsholds the condition, the entries and the mark.
- fixed
- new
ata-validator/compiled-bare-rich:bareRich(mod, schema), the bare wrapper for a validator that reads ata's own errors, the same errors asfromCompiledObjectwithout Standard Schema and the extension hook.@ata-project/unpluginuses it for a validator only ever called; the two-validator form app went from 13.5 to 10.7 KB gzipped. Held to a default Validator intests/test_compiled_bare.js. - new
compiledModuleFor(schema, { verdictOnly: true })writes the module for a reader that only ever callsisValid(), without its error collector (itsvalidate()hands out the abort-early stub): 954 to 539 bytes gzipped on a one-field object.@ata-project/unpluginwrites verdict-wrapper modules this way, and keeps the full module forverify. TherichErrors: falseform app went from 3.8 to 3.5 KB gzipped. - new
ata-validator/compiled-bare:bareVerdict(mod)andbarePlain(mod), the smallest compiled-module wrappers (315 bytes gzipped for both), for a validator the bundler plugin sees used only through direct calls ofvalidate()orisValidObject()and never handed to other code, so nothing reads~standardor registers a check on it. Defaults are filled with the module's own pass;barePlainhands out the plain errors of aplainErrorsmodule.@ata-project/unpluginuses them where that holds: the form app withrichErrors: falsewent from 6.1 to 3.8 KB gzipped.tests/test_compiled_bare.jsholds them to the runtime on 22,315 answers over the suite. - new
ata-validator/compiled-plain:fromCompiledPlain(mod, schema, { richErrors: false }), the compiled-module wrapper for a Validator built withrichErrors: false, around a module written withcompiledModuleFor(schema, { plainErrors: true }), whose one-pass program writes errors in that plain shape and order. It hands them out as they are and carries no rejection classes, enrichment or suggestions: 2.6 KB gzipped.@ata-project/unplugincompiles such a validator away with it, where it kept it on the runtime before. A form app with one validator that reads errors and one that asks for a boolean went from 16.7 KB gzipped on 1.61.0 to 6.1 withrichErrors: false.tests/test_compiled_plain.jsholds it to the runtime on 17,852 answers over the suite in five dialects. Standard Schema reaches the enrichment only for a native engine's message-less error, now throughlib/native-message.js, which the browser build maps to a stub, since a browser has no native engine. - new
ata-validator/compiled-object:fromCompiledObject(mod, schema), the compiled-module wrapper withoutvalidateJSON()andisValidJSON(), for code that reads errors but never validates JSON text. The position map those entry points use to frame an error in the text stays out of the bundle: 10.9 KB gzipped against 12.8 for the full wrapper.@ata-project/unpluginpicks it where the replaced validator is never asked to validate JSON text.tests/test_compiled_parity.jsholds it to the runtime over the suite. - fixed
ata verifyalso tries, for every bound a schema states (minimum,maximum, the exclusive ones,multipleOf,minLength,maxLength), a value on either side of it, on a document the runtime accepts. Its random documents rarely land on a bound, so a module that read one off by one (a minimum of 13 written as< 12) answered all 200 of them as the runtime did and passed; it is now caught, with the runtime saying invalid and the module valid.tests/test_verify.jsholds the case.
- new
- 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 in the notes
- 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
237 other lines we could not sort, mostly version bumps
- 1.61.2
Tier 0, the shared verdict for flat object schemas, checks
formaton a string property with the table the interpreted engine reads (formats.formatChecks, moved out oflib/interpreter-core.js) or the caller's own function, so a schema with an email field stays on it; and on the interpreted engine it answers the verdict whether or notvalidate()came first, whichvalidate()'s own verdict-first path then uses too. On Node's interpreted engine, the ten-field user schema withformat: email: an accepted document 86 to 70 ns, a rejected one 33 to 17,validate()91 to 75 and 35 to 19. Under Bun's ahead-of-time compiler, with the fix below,isValidObject()78 to 48 ns andvalidate()94 to 51, against 483 for zod 4 there.tests/test_tier0_format.jsholds tier 0, the interpreted engine and the generated code to the same verdicts on the 6,558 format cases of the official suite.ata-validator/litegrows by 85 bytes gzipped. - 1.61.1
A browser build carries no code for the native engine:
lib/native-positions.js,lib/native-codes.jsandlib/native-message.jsmap to one stub (lib/native.browser.js), since the native loader returns null there. The runtime's code generator chunk went from 78,733 to 78,237 bytes gzipped and the whole runtime from 139,438 to 138,944; the first load is unchanged at 54,383. - 1.61.1
A browser's generator checks an email address literal with the IPv6 and IPv4 parsers it carries, and the expression a compiled module uses (
lib/email-literal.js) maps to nothing there. The same answers on either form (tests/test_ipv6_expression.js). - 1.61.1
The format codes in
lib/error-codes.jsare written by one loop;get()andall(), which only tests and scripts read, are gone from it. The table is unchanged, key order included. - 1.61.1
ata-validator/compiled-bareis an ES module of its own rather than a re-export of the CommonJS file, so a bundler adds no interop code: 662 to 315 bytes gzipped. - 1.61.1
The rejection
validateJSON()returns moved tolib/json-rejections.js, loaded with the first rejection that carries JSON text, sovalidate()through a compiled wrapper does not bundle it (532 bytes gzipped) and a process that never rejects JSON text no longer loadslib/diagnostic-source.js. The native engine's ordinal table moved tolib/native-codes.js, which a browser build maps to a stub.ata-validator/litewent from 57,790 to 57,769 bytes gzipped. - 1.61.1
A built-in format under
type: stringis no longer repeated in the branch for a value of another type, where it could never fire. - 1.61.1
A compiled module that checks
format: emailreads an address literal (a@[IPv6:...],a@[1.2.3.4]) with two regular expressions instead of the IPv6 and IPv4 parsers it carried inline, which were 72% of the check's source: an email schema's module went from 1,743 to 1,524 bytes gzipped. The answers are the same (tests/test_ipv6_expression.js: the expressions againstipv6()andipv4()on 400,000 near-addresses each, the emitted check againstformats.emailon 100,000 literals); the expression lives with the code generator, so the interpreted engine andata-validator/litedo not carry it. - 1.61.1
The compiled wrappers fill defaults with the module's own
applyDefaults(exported by modules from 1.58.0 on when the schema has a default, the same pass as the runtime's), so the defaults engine is no longer in a bundle that wraps current modules: the verdict wrapper went from 2.7 to 1.6 KB gzipped, the full one from 13.8 to 12.8. A module from before 1.58.0, or without its version stamp, takes the runtime's pass as before; in the browser build such a module with defaults is refused with an error that says to rebuild it rather than run without its defaults.tests/test_compiled_defaults_source.js. - 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.
13 internal lines (docs, tests, tooling) left out.