Packages

ata-validator

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

Your version

1.54.0

15 Oct 2026Where it was published

1.61.3 is the newest release of 1.x.

In short

7 fixed19 changed

Release note

Fixed

  • The uri, uri-reference, iri and iri-reference formats follow RFC 3986 in full. They checked the characters and, for uri, the shape of the authority; a bracketed host was not read, so http://[::ffff:01.2.3.4] passed, and uri-reference checked only the characters, so //example.com:abc/p, //a@b@example.com/, /[::1] and 1:b passed. 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 in lib/uri-core.js and is the same text in the interpreted engine, generated code and standalone modules. The uri expression 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 documents uri and iri give the same answers as before; uri-reference now 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.

  • uri-template: a % in a literal or a variable name must start a percent-encoded triplet (a%, a%4, a%GG, {%} were accepted).

  • idn-email: a domain label that starts or ends with a hyphen is refused, as email refuses it (the official suite's 7de0e6a adds these cases).

  • email: the inside of a quoted local part is checked ("test\©"@iana.org was accepted, a quoted pair takes one printable ASCII character), and the IPv6: tag of an address literal is read in any case (a@[ipv6:::1] was rejected).

  • A node without type: object checked an array against its properties: [1, 2] was rejected by { properties: { length: { maximum: 1 } } }, through the array's own length, 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 (length or 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.js covers both on 189 documents.

  • Draft-04 is read as draft-04. A schema that declared it was read as 2020-12: id did not set the base URI, tuple items, additionalItems and dependencies were not applied, and const, contains and propertyNames, which draft-04 does not have, were. It now goes through the draft-06 rules plus its own (id for $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:suite runs them. SchemaStore's draft-04 schemas answer their sample documents as before.

  • A declared property holding undefined still got different answers in 1.53.0: the generated code checked the value where the key was required, where the node had no type, and inside if, not and similar subschemas, and the engines disagreed on whether a required key holding undefined was 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 passes required: ['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.js covers 21 shapes on every engine, with and without code generation.

Changed

  • Two schemas that generate the same code no longer slow each other down. V8 keeps one compiled function per source text of a new Function call, with one set of inline caches, so two copies of a record schema under different $ids, each validating lists of a different shape, ran each other's caches megamorphic: 500 records took 2.8 us alone and 18.0 with the twin in the same process. Every program compiled at run time now opens with a comment that differs per compile, 2.8 us either way. Standalone builds carry no such comment, so a build is the same text whatever was compiled before it.

  • Reading the errors of a list of records with many optional properties, where the records do not share one shape, took 20.6 ms for 2000 records and now takes 0.57. The error program guards each record with a verdict and collects only the one that fails; the guard for a 150-property record was written one read per property, 16.9 KB, over the size the program accepts, so there was no guard and every record was collected in full with reads that change shape at each one. The guard now takes the key-loop form the verdict uses for such records, 10 KB, and runs only on a list whose first, middle and last records list different keys: records of one shape are read as before (16 us for the same 2000). On the way the key loop's switch, pushed as separate lines, read switch(k){;case once joined with ;, a syntax error that made the rich error program fall back to the plain one without a word; it is one line now and tests/test_rich_key_loop_guard.js holds the compile.

  • A oneOf under type: object writes its branches for an object already checked, without the type test and the second own-key read each branch made for its required keys (vercel's now.json spends 23% of a validation in two such branches). A branch reached where the type is not known keeps its test, as a separate function.

  • A pattern that only names keys, ^cdn$ or ^(cdn|Cdn)$, is a string comparison in generated code, and .* is no test at all. appsettings carries eight such patterns and answered each with a regex run per key; its samples validate 15% faster. tests/test_pattern_literal_keys.js holds the shapes that must still be a regex (^a|b$, ^(a|b)c$, a $ not at the end).

  • The linear regex engine's matcher loop reads its state tables through locals and reads one flag per character for both a match and a dead state. The tables live in the matcher's closure and were reloaded on every character, about a third of the loop; SchemaStore's popxf-1.0, whose eleven patterns all run on this engine, validates its samples 18% faster. The engine's answers are unchanged (88000 random strings against the platform RegExp, 0 differences).

  • A definition on a reference cycle that refers back to the root ($ref: "#") compiles. Such a definition is a function above the entry, where the entry is not in scope, so the schema declined code generation and ran on the interpreter: SchemaStore's jsone, the one schema with sample documents that did, was 0.60x the default validator's speed. The definition now reaches the root through a forwarder the entry fills in, named per compile, so two such schemas in one bundle do not share it. tests/test_def_root_forwarder.js holds generated code, a standalone module and a two-schema bundle to the interpreter's answers.

  • An array of records with 32 or more optional properties is checked one of two ways per call. When three of its records (first, middle, last) list their keys alike, which records of one shape do, each declared property is read as before; when they do not, each record is checked from its own keys. Reading every declared property is fastest while the records share a shape and slowest once their shapes vary, because V8's property reads then stop being specialised. On 2000 records of 150 typed fields, records that each carry a different handful of keys go from 20.13 ms to 0.29 ms an array, and records of one shape from 7.41 to 7.19 µs. tests/test_item_key_loop.js holds both forms to the interpreter's answers on 648 arrays. Error lists for such arrays are still built the old way.

  • A patternProperties entry whose schema checks nothing ({} or true), as OpenAPI's ^x- extensions are on nearly every object, no longer writes a loop over the object's keys: the loop read each matching key's value and did nothing with it. A valid OpenAPI 3.1 document goes from 4.49 to 3.47 µs and a rejected one with its errors read from 3.73 to 3.35 µs; the first validation is unchanged (10.82 against 10.81 ms, median of 21). A pattern that still checks something, a propertyNames and additionalProperties keep the loop.

  • OpenAPI 3.1's own schema runs on generated code. It has a property named examples whose schema is a $ref, and the passes that rewrite references skipped examples, default, enum and const as value keywords even where they are property names, so the copy pointed at a definition it no longer had and the whole schema fell back to the interpreted engine. A valid OpenAPI document goes from 9.81 to 4.62 µs, and to 3.47 µs with the next entry, a rejected one with its errors read from 14.06 to 3.85 µs; the first validation is unchanged (10.83 to 10.69 ms). tests/test_name_map_value_keys.js holds those four names as properties, through $dynamicRef and through a reference into another document, to generated code and to the interpreter's answers.

  • Reading a rejected document's errors costs about half what it did when the list has more than one error. The detail text is worked out where each error site is compiled, as far as the site allows, instead of from the keyword at every failure, and the list built where the errors happen says whether it came out in schema order, so it is sorted only when it did not. A migration-style schema with two errors goes from 159.9 to 79.6 ns a document; the request benchmark's rejected body with its error list read from 135.3 to 96.1 µs against 1.53.0. The errors are unchanged: tests/test_rich_errors_generated.js compares them with enrich() on 10,553 rejections, and SchemaStore's 26,563 rejections keep their order with the order check on.

  • A fresh process loads about 68 KB less before its first answer: the draft-07 and 2019-09 normalizers, the $vocabulary reader, the runtime format functions and the small-schema tier load where they are first used. A 2020-12 schema's first validated document comes 6.9% sooner than with 1.53.0 (7.56 to 7.04 ms, median of 41 processes); a draft-07 schema that needs normalizing, which still loads them, 7.1% sooner (7.12 to 6.62 ms).

  • A definition that refers to itself on the same value through allOf or anyOf (X: { allOf: [{ $ref: X }] }) no longer sends the whole schema to the interpreted engine. SchemaStore's cloudify, 284 KB, goes from 38 to 9.1 µs a document, against 10.6 for the default validator. A not, if, then or else subschema is a function written once instead of a closure made on every call, and is told whether the value is an ordinary object instead of reading its prototype again: minecraft-predicate's 25 if/then pairs go from 126 to 99 ns. A recursive anyOf or oneOf branch is written in place instead of as a closure built on every call: jsonld from 61 to 46 ns.

  • uniqueItems and const/enum on objects compare keys and scalar values before nested ones, from the last key back, so two items that differ in a name or an id are told apart without walking their nested lists first. rust-project's crate list goes from 301 to 126 ns a document, against 160 for the default validator. The generated code's copy and the interpreted engine's stay one function, held by tests/test_unique_items_error_pair.js.

  • A schema that refers to itself, or holds a definition on a cycle, declares its guarded functions once instead of on every call: the verdict and the forms validate() builds from it made three closures per document. On SchemaStore's samples component.json goes from 52 to 40 ns a document and jsonld from 46 to 38, against 52 and 29 for the default validator.

  • An enum of 64 or more strings is checked with one Set lookup instead of a comparison per value: jscpd's 224 values go from 1462 to 237 ns a document, cryproj's 2461 from 1955 to 69. Below 64 the comparisons stay, since on real documents they were faster. A single-branch anyOf or oneOf is written as its branch, or not at all when an allOf beside it applies the same $ref. On SchemaStore's devinit schemas, whose items read { allOf: [{ $ref: R }], anyOf: [{ $ref: R }] }, a document goes from 2.29 to 1.10 µs (devinit 4.0), 2.12 to 1.04 (3.0) and 2.07 to 0.92 (1.0), against 2.10, 1.97 and 1.91 for the default validator.

  • ^[class]+$, ^[class]{m,n}$ and longer ^[class]{n}$ patterns over ASCII letters, digits, _ and - are checked by a function that reads each character once and looks it up in a 128-entry table, instead of comparing it against each range in an arrow function built per call. component.json's verdict goes from 30.5 to 26.9 ns, amx-muse's from 28.8 to 25.2 (the default validator: 29.8 and 28.8).

  • More patterns run on the platform RegExp instead of the linear-time engine, which costs three to five times as much per character. A loop whose every iteration ends with a character nothing else in it matches (^([a-zA-Z_$][\w$]*\.)*[a-zA-Z_$][\w$]*$, ^/?([^/]+/)*[^/]+/?$) and a pattern with one bounded repeat that can vary (^[a-z0-9_-]{1,128}$) are linear on a backtracking engine too. Eight of SchemaStore's 1237 patterns move; they answer as the linear engine does on 24,000 inputs and take under a millisecond on inputs built to make a backtracking engine retry. bungee-plugin goes from 134 to 80 ns a document, imgbotconfig from 147 to 89, apple-app-site-association from 489 to 310.

  • The browser bundle no longer carries what only ata-validator/compat uses: coercion where each type is checked and the discriminator are in their own file, which compat installs, and the schema scan's compat questions are asked only when compat is loaded. An app that imports only Validator goes from 128.5 to 127.6 KB gzipped with esbuild, ata-validator/lite from 54.8 to 53.8 KB. compat behaves as before: express-openapi-validator's suite still passes, and verdicts and coerced data still match the reference on 2400 generated documents.

Size

  • Measured against 1.53.0, gzip. The full Validator browser bundle (index.browser.mjs) is 131.9 KB, up from 128.5: the generators that build errors where they fail, the key-loop forms, the per-compile tag and the URI walk all land in it. A standalone module that checks a uri is 2.7 KB, up from 2.1, since the walk replaced a character scan; one that checks an email is 2.3 KB, up from 2.2. ata-validator/lite is 54844 bytes, 1.53.0 was 54767: 77 bytes, the URI walk against the character scans it replaced. The core tarball is 450 KB packed, 439 in 1.53.0, most of it the draft-04 meta-schema. idn-hostname and the xn-- half of hostname are still not asserted; a Punycode and label-rule check was written and measured at 1.2 KB on lite and 1.4 KB on every standalone module with an email or hostname, and held back for that cost.