ata-validator
1.61.3 · 29 Oct 2026MIT21.8k / weekRepositoryHomepagenpmAtom feed
1.61.3 is the newest release of 1.x.
In short
7 fixed19 changed
Release note
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.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, asemailrefuses it (the official suite's 7de0e6a adds these cases).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).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.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.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.
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 Functioncall, 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){;caseonce 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 andtests/test_rich_key_loop_guard.jsholds the compile.A oneOf under
type: objectwrites 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.jsholds 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.jsholds 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.jsholds both forms to the interpreter's answers on 648 arrays. Error lists for such arrays are still built the old way.A
patternPropertiesentry whose schema checks nothing ({}ortrue), 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, apropertyNamesandadditionalPropertieskeep the loop.OpenAPI 3.1's own schema runs on generated code. It has a property named
exampleswhose schema is a$ref, and the passes that rewrite references skippedexamples,default,enumandconstas 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.jsholds those four names as properties, through$dynamicRefand 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
detailtext 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.jscompares them withenrich()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
$vocabularyreader, 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
allOforanyOf(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. Anot,if,thenorelsesubschema 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.uniqueItemsandconst/enumon 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 bytests/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/compatuses: coercion where eachtypeis 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 onlyValidatorgoes from 128.5 to 127.6 KB gzipped with esbuild,ata-validator/litefrom 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
Validatorbrowser 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 auriis 2.7 KB, up from 2.1, since the walk replaced a character scan; one that checks anemailis 2.3 KB, up from 2.2.ata-validator/liteis 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-hostnameand thexn--half ofhostnameare 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.