Packages

ata-validator

1.61.3 · 29 Oct 2026MIT21.8k / weekRepositoryHomepagenpmAtom feed

Your version

1.58.0

21 Oct 2026Where it was published

1.61.3 is the newest release of 1.x.

In short

4 security9 changed

  • security

    An object in enum or const could 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 passed enum: [{ "a": 1, "b": 2 }]. Member names are now escaped as JSON strings, which makes the text one-to-one. The same text decided uniqueItems, where it made two different objects count as equal and rejected the array.

  • security

    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_exceeded in 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 as validate() does at any depth. That route was faster for such schemas, not slower: 4,638 to 2,495 ns for isValid on a 20-level document.

  • security

    isValidParallel and countValid read up to 64 bytes past the end of the caller's buffer for a line ending near it, and isValidPrepadded(buffer, length) read as many bytes as length said whatever the buffer's size. A line without 64 bytes of the buffer after it is now copied to a padded buffer first, and isValidPrepadded returns 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.

  • security

    isValidParallel could return false for a valid line: its workers wrote their answers into a std::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 with validate().

Release note

Security

These are in the optional native engine (@ata-validator/native-*, or a build from source). Without it none of them applies. With it, the APIs that ask the native walker are isValid, isValidPrepadded, isValidNDJSON, isValidParallel, countValid, batchIsValid, and validateJSON/isValidJSON on documents of 8 KB or more; validate() and isValidObject() never ask it.

  • security

    An object in enum or const could 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 passed enum: [{ "a": 1, "b": 2 }]. Member names are now escaped as JSON strings, which makes the text one-to-one. The same text decided uniqueItems, where it made two different objects count as equal and rejected the array.

  • security

    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_exceeded in 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 as validate() does at any depth. That route was faster for such schemas, not slower: 4,638 to 2,495 ns for isValid on a 20-level document.

  • security

    isValidParallel and countValid read up to 64 bytes past the end of the caller's buffer for a line ending near it, and isValidPrepadded(buffer, length) read as many bytes as length said whatever the buffer's size. A line without 64 bytes of the buffer after it is now copied to a padded buffer first, and isValidPrepadded returns 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.

  • security

    isValidParallel could return false for a valid line: its workers wrote their answers into a std::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 with validate().

tests/test_native_security.js holds each one through the addon and through the public APIs, and tests/test.cpp the first two in the C++ API. singleheader/ is regenerated from the current sources.

Changed

  • default values are filled in every schema applied to a value, as the default validator's useDefaults fills them: behind a $ref into the same document (recursive ones included), in each allOf member, in array elements under items and prefixItems, under additionalProperties and patternProperties for the keys they cover, and under dependentSchemas for the keys present. The pass stopped at properties, so a required property whose default sat behind a reference or in an array element was rejected where the default validator accepts it. anyOf, oneOf and not fill nothing, as there. if/then/else fill 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 with oneOf, anyOf, if or not. 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's applyDefaults, which now embeds the closure pass where the generated one declines: 405 of 878 SchemaStore modules export applyDefaults, up from 289. tests/test_defaults_reach.js covers each place, against values taken from the default validator, and 300 generated schemas on all three passes.

  • 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 useDefaults rejects 16 of them. The 17th fills layout: true where a sibling then says not: { required: ["layout"] }: the default validator checks allOf before it fills properties, ata fills first. useDefaults: false leaves the input as it is.

  • 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 RangeError on it. An empty Renovate configuration fills to 22,774 bytes, as before.

  • 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.

  • 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.

  • 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 of lib/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 with ataResolver was 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.js covers the split and the answers before and after.

Measured on the 564 SchemaStore schemas with a default, constructing each and validating its first sample: 1,462 to 1,327 ms in total, and with code generation blocked 934 to 991 ms. The blocked figure is the closure pass filling what it now reaches: a 400 KB SARIF log gains 4,130 values. The per-schema gate over SchemaStore and the request gate show no change beyond noise.

Size

  • The browser build's first load is 4,292 bytes smaller gzipped (58,245 to 53,953): the defaults pass and its guard added 1,077 and moving the plan compiler out took 5,369. The plan compiler's chunk is 6,334 bytes.

  • ata-validator/lite is 57,380 bytes gzipped (up 1,213) where it is bundled without splitting, as tests/test_lite_bundle_size.js does: the plan compiler stays in, and the code that loads it is added. With a bundler that splits, its first load is 51,839, from 56,277 in 1.57.0. The Workers bundle is 58,577 (up 1,013).

  • The package is 470.3 KB packed.