Packages

ata-validator

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

Your version

173 stable releases, newest first, by release line. Notes are quoted as the maintainers published them.

  1. 1.61.3

    29 Oct 2026

    Reading the note…

  2. 1.61.2

    28 Oct 2026

    Reading the note…

  3. 1.61.1

    26 Oct 2026

    Reading the note…

  4. 1.61.0

    26 Oct 2026
    • A schema with a default anywhere (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. One required error on a four-field user with a defaulted newsletter went 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).

    • 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=1 turns 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.js holds the conditions.

    • ata-validator/build exports bunTarget (the schema as a TypeScript type and the .bun.ts module ata build --target bun writes; with tags: true the split into a type with Bun's Schema tags and a residual schema, made for the typed validator the compiler's pull request carried until 2026-10-25) and verify (verifyOne, verifySource, generate), for a build plugin that replaces new Validator(schema) inside a Bun.build({ compile: { aot: true } }) and holds what it wrote against the runtime.

  5. 1.60.0

    24 Oct 2026
    • A validator the build compiled away (fromCompiled around a module written ahead of time, what @ata-project/unplugin puts in place of new Validator(schema)) carries Standard Schema V1 as the runtime Validator does: the same ~standard object, built by lib/standard-schema.js for 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.js compares the two.

    • toStandaloneModule(schema, { coerceTypes }) and toStandaloneModule(validator) for a validator built with coerceTypes or removeAdditional wrote 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, as bundleStandalone does since 1.59.0, and refuses where that pass cannot be written into a module. fastify-ata/standalone builds its files through this call.

  6. 1.59.0

    24 Oct 2026
    • bundleStandalone(schemas, { order: true }) hands each validator's errors out in the runtime's order, as toStandaloneModule does with the same option. rjsf's precompiled validator writes its module with bundleStandalone, and its error list showed an additionalProperties error before the properties errors where the runtime validator shows it after. tests/test_aot_order_defaults.js holds 553 bundled groups of the 2020-12 and draft-07 suites to the runtime's errors, in order.

    • 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.js compares 8400 generated documents with the runtime: the data afterwards, the verdict and the errors in order.

    • bundleStandalone left coerceTypes and removeAdditional out without a word, so a bundle built with coerceTypes: true rejected { 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 with useDefaults: true fills 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), bundleStandalone throws instead of writing a module that answers differently. Without these options a bundle checks the data as it is, as before.

  7. 1.58.1

    22 Oct 2026

    Reading the note…

  8. 1.58.0

    21 Oct 2026
    • 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.

  9. 1.57.0

    20 Oct 2026
    • In the browser build, a page's first check no longer pays for V8 compiling the engine, nor for building the schema's engine. Both move to when the page is idle (requestIdleCallback, or 200 ms later where there is none, as in Safari and in workers): the first validator a page builds schedules one check of a small schema of ata's own, once per page, and then the engine for each validator built since, at most eight, which a validator adopts at its first check only if every input is still the same, its schema text included. addSchema() or a change to the schema object between construction and the first check is honored as before, a schema with custom keywords is left to its first check, since their compile functions are the caller's code, and nothing on a validator changes until it is used. The warm-up is dropped when a validator has compiled before the page is idle. Measured in Chrome with @hookform/resolvers on an eight-field form, the first submit 300 ms after the form was built went from 6.43 to 0.88 ms, and from 18.56 to 2.48 ms with the CPU slowed 4x as Lighthouse does for mobile. A submit before the page has been idle went from 3.97 to 3.66 ms, and from 18.93 to 17.20 ms with the CPU slowed. Node, lite and edge do not carry the idle work. tests/test_browser_warmup.js covers the warm-up, the adoption and each case where an engine built while idle must not be used.

    • The interpreted engine gives a pattern to the platform RegExp when it has no group or alternation, few bounded choices in all, and at most one unbounded repeat, on one character or class and only when the pattern starts with ^: such a pattern runs in linear time on a backtracking engine, as the code generator already decided for these patterns (lib/regex-linear.js). The linear engine took 40 to 100 us to build a pattern's automaton, a RegExp about one. On lite, a form's first check went from 3.83 to 3.49 ms and a valid document from 205.8 to 193.0 ns. tests/test_regex_plain_linear.js holds that every pattern taking the RegExp gets one from the code generator too, so both engines answer with the same matcher, and that the answers equal the linear engine's on generated strings.

    • lib/rejections.js and the schema order and diagnostic modules it brings load at the first compile, where they are first used, rather than when the package loads. Importing the browser build went from 4.41 to 4.36 ms in Node.

  10. 1.56.0

    18 Oct 2026
    • The browser build (the browser export condition, which Vite, webpack, Rollup and esbuild use for a page) no longer puts the code generator in the page. Every schema starts on the interpreted engine, and a validator called a thousand times within a second loads the generator through a dynamic import, which bundlers split into a chunk of its own, and is rebuilt on generated code in place, one validator per idle task, so that several turning hot together do not hold the main thread (lib/tiering.js). Calls spread out further, a form checked as the user types, never load it: there generated code would save tens of nanoseconds a check against about 6 ms to load the generator and compile. Once rebuilt, a validator is as fast as one compiled from the start (16 ns a check on a form schema, both ways). The answers are the same on both engines, valid and errors alike. On a react-jsonschema-form page the validator's share of the first load goes from 134.7 KB to 57.5 KB gzipped with esbuild's splitting (205.9 KB for the whole page with Vite, 280.2 before); the generator chunk is 77 to 80 KB when it loads. A bundler that does not split chunks puts it back in the page, as before. Where code generation is blocked the chunk is never fetched.

    • ata-validator/codegen registers the generator at once, for a page that wants generated code from the first call.

    • The docs no longer point pages under a strict Content-Security-Policy or Manifest V3 extensions to ata-validator/lite: the browser build checks whether generated code can run before it fetches the generator, so ata-validator covers them. ata-validator/lite stays and works as before for code that imports it.

  11. 1.55.0

    17 Oct 2026
    • bundleStandalone and bundleCompact modules threw ReferenceError: _cpLen is not defined on a string whose minLength or maxLength needed counting code points: a one-character value under minLength: 1, an emoji under a maxLength. The length checks call a code-point counter that toStandaloneModule embeds 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 uses bundleStandalone: typing one character into a required name field threw. The bundles now embed the counter when their code calls it; tests/test_bundle_cplen.js runs both bundles in both formats.

    • The interpreted engine, which answers where code generation is blocked (Cloudflare Workers, Deno Deploy, a strict Content-Security-Policy), does the same work in fewer calls. A leaf of one type and only that type's keywords ({ type: 'string', maxLength: 80 }, { type: 'integer', minimum: 1 }) is one closure over typeof and the check, with no type bits computed first; an object's value checks run inside its property step instead of beside it; and a required key that is also a declared property is counted while the properties are walked, not looked up again. A declared property of an ordinary object (one whose prototype is Object.prototype) is read directly where its name is not on that prototype, instead of after an own-property test; a key such as constructor or toString, and any other object, keep the test. An enum of primitive values is one Set lookup, and a minLength with a maxLength is one closure that decides most strings from their UTF-16 length. On the request benchmark with code generation blocked, against 1.53.0: accepting the body 240 to 155 µs, rejecting it 106 to 73, the error list read 577 to 506; the hot loop 195 to 142 ns a document. A hand-written closure tree for the same schema measures 136 ns, so the engine is within 5% of what closures can do; the generated code's 20 ns is a different mechanism. ata-validator/lite, which is this engine, grows from 54.8 to 55.5 KB gzipped for it. tests/test_plan_compiles.js holds the closure-tree compiler to every schema of the official suite, since the engine quietly falls back to its plain walk when the compiler throws, and did so for a while during this work with every test green; ATA_DEBUG=1 now reports that fallback.

    • A compact module (ata build --errors auto, toCompactModule, the bundler plugin's sharedErrors) takes the RFC 3986 walk behind the URI formats from the error runtime it already imports (uriWalk), instead of carrying the factory's text. 1.54.0 put that text, about 1.3 KB gzipped, in every module with a uri, uri-reference, iri or iri-reference field: forty SchemaStore schemas built that way went from 213.3 to 224.9 KB gzipped, and measure 217.9 now (the rest is the 1.54.0 email and record-list code, which stays). A self-contained module still carries it, since it imports nothing. ata-validator/error-runtime exports uriWalk for the same reason; a compact module written by 1.55 needs that export, so build and runtime come from one version, as before.

  12. 1.54.0

    15 Oct 2026
    • 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).

  13. 1.53.0

    11 Oct 2026
    • 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/compat gives every keyword this context and it.schemaPath, which is what express-openapi-validator's readOnly and writeOnly keywords use to strip read-only properties from a request.

    • ata-validator/compat takes what a library sets up on it in its own code: the formats plugin's addFormats() 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 its id as $id and 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 on compat with one change of its own (a portable x-eov-type) and validates requests against GitHub's and Stripe's API descriptions, which the default validator cannot load.

    • ata-validator/compat does what the reference does while it validates, for the schemas that need it. coerceTypes converts a value where each type is checked, so it reaches values behind anyOf, oneOf, allOf and $ref; useDefaults fills defaults behind allOf and $ref; the discriminator option applies the oneOf branch the tag names; a schema registered under a fragment-only key such as #/components/schemas/Pet is found by a $ref of that string; a format registered with type: 'number' is checked on numbers; and custom keywords take before and post to order keywords that change the data. Errors name the schema a $ref points to, and oneOf and anyOf with 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 and coerceTypes: 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.

  14. 1.52.0

    11 Oct 2026
    • A oneOf whose branches each allow only their own properties ({ properties: {...}, additionalProperties: false }, no property in two branches) is answered by the branch the object's first key names instead of trying every branch. Traefik's file-provider schemas, 25 middleware branches, go from 10.8 to 1.2 µs a document. Values that are not objects, and objects with no keys, still try every branch.

    • A small object definition referenced from many places in a large schema is written as several functions with the same body, each serving at most four references. One function called with objects of dozens of shapes falls off V8's fast property reads; SchemaStore's detekt sends 72 shapes through one rule definition and goes from 6.3 to 1.1 µs a document. Definitions without properties are not copied.

    • The linear-time pattern engine, which runs the patterns the platform RegExp cannot be trusted with, steps through one flat transition table of 16-bit entries instead of a state object per character: 25 to 35% less per test on SchemaStore's patterns, and less memory than before (17.9 against 21.2 KB per pattern in use). dart-build goes from 2.82 to 1.96 µs a document, popxf from 6.44 to 4.46, apple-app-site-association from 0.73 to 0.50.

  15. 1.51.0

    11 Oct 2026
    • ata build builds schemas that reference each other by $id. A schema whose references leave it gets the other inputs registered beside it, as the schemas option would; one that resolves within itself is built as before. Such schemas used to be skipped as not AOT-compatible: on 64 schemas that reference each other 1124 times, 2 were built, now 63 are (the 64th holds only $defs).

    • With --mode schema those modules name their schema by $id, and the schemas are written once to ata-registry.mjs beside the runtime. For the 19 of those schemas one application builds browser validators for, that is 63.6 KB gzipped, minified, against 273.6 KB for self-contained modules, which each carry what they reference. The answers are the same: on 59,724 document and schema pairs from the project's fixtures, verdicts match the self-contained modules and another validator, and error lists match the runtime Validator. toSchemaModule(validator, { runtime, registry }) and registrySource(schemas) in ata-validator/aot do it by hand.

    • ata build --base-uri <uri> names every input without an $id by the URI and its file name, so schemas that reference a shared file by URL or relative path build. Kubernetes schemas reference one _definitions.json that declares no $id: none of them built before, now each does.

  16. 1.50.0

    10 Oct 2026
    • ata-validator leaves its code generator out where generated code cannot run. Cloudflare Workers and Vercel Edge refuse new Function, so the generator could only fail there and fall back; a bundler building for them sets the workerd or edge-light export condition, and the package now resolves to an entry without it (index.edge.js). Nothing changes in the code that imports it: the same names, and with code generation blocked the same verdicts and errors as the full package, 2230 cases of the 2020-12 and draft-07 suites compared. A Worker with one validated route, bundled by wrangler 4.149.0, went from 912.8 KiB (179.1 KiB gzipped) to 376.4 KiB (70.4 KiB gzipped), and answered the same requests under wrangler dev. Modules from ata build load through it; Validator.bundle and the other emitters point to ata-validator/aot, as in a browser bundle, where they already did. ata-validator/lite stays for pages with a Content-Security-Policy that blocks unsafe-eval and for browser extensions, which no condition identifies. The package's index.js is split for it: the generator's paths are in lib/codegen-engine.js and the public names in lib/public-api.js; requiring the package takes the same time (4.52 against 4.54 ms, fresh processes).

    • Compact modules for production builds. ata build --no-source (or with NODE_ENV=production) can write each module as its verdict function and its schema without the descriptive keywords, and take error detail from one file the build writes beside the modules, ata-errors.mjs, which every module of the build imports. The runtime walks the schema with the interpreted engine when a document fails, so it only runs on rejections. The build chooses by size, per module, and writes the runtime only when the modules that use it save more than it weighs; one small schema keeps its self-contained module, and --errors inline|shared overrides the choice. On seven SchemaStore schemas (abc-supply-plan 13, airlock-microgateway 3, aspire 8, babelrc, github-workflow, prettierrc, tsconfig) imported into one app and minified, the validation code goes from 148.9 to 63.3 KB gzipped and from 35.8 to 13.8 ms to import with code generation blocked (Apple M4 Pro, Node 25.2.1). Forty SchemaStore schemas in one app go from 513.8 to 211.8 KB gzipped (5792.5 to 1566.3 KB minified) and from 61.7 ms to 32.0 ms to import. The runtime carries only what the build's schemas use: format checks, the linear-time pattern engine and deep equality each stay out when no schema has the keyword that reaches them, so the runtime is 11.3 KB gzipped for schemas with none of them and 18.6 KB with all three; 336 of the 445 SchemaStore schemas with sample documents run on a runtime with at least one left out. A compact module also takes the pattern engine from the runtime instead of embedding its own copy, 2.9 KB gzipped, which 147 of 875 SchemaStore modules carried each. On Microsoft's Accessibility Insights assessment schema, the module and its runtime together are 33.6 KB gzipped against 75.9 for the self-contained module. The cost is on rejections: the errors come from an interpreted walk of the document, 398 ns for a rejected request body against a four-field user schema where the self-contained module takes 52, and 57 µs for a 22 KB assessment file with one wrong field where it takes 21 (with several wrong fields, 63 against 94). A build that cares more about rejection speed than size passes --errors inline. toCompactModule, compactDecline and errorRuntimeSource in ata-validator/aot do the same by hand.

    • ata build --mode schema: schema modules, for pages where the bytes shipped matter more than the time per check. A module is its schema without the descriptive keywords and a few lines, and the verdict comes from the shared runtime too, the interpreted engine with its closure tree for a schema used often. Forty SchemaStore schemas in one app, minified, go to 101.6 KB gzipped (212.6 as compact modules, 513.8 self-contained), and loading them and checking each once takes 23.0 ms against 35.5 for compact modules. Once warm a check costs about five times what generated code costs, 3289 against 590 ns a document on SchemaStore samples. The answers are the runtime Validator's: no verdict differs on the official suites in five dialects or on 13741 SchemaStore documents, and every error list of the 11362 rejected ones is the runtime's. toSchemaModule in ata-validator/aot does it by hand.

  17. 1.49.0

    10 Oct 2026
    • More patterns run on the platform RegExp instead of the linear-time engine. The rule in lib/regex-linear.js now also accepts a pattern that is deterministic around its loops, on the Glushkov construction: the characters that can follow a position inside a loop are pairwise disjoint, a position reaches a follower by one route only (so (a+)+ and (\w+\s?)* stay out), no loop body matches the empty string, and the pattern is anchored. That takes in dotted identifiers, ^[^/]+/[^/]+$, version numbers and (ab)*. Over SchemaStore's 1277 patterns 90 more go native, with no answer difference on 18000 random inputs and no slow run on 20000-character adversarial probes; the engine is 3.5 to 5x the RegExp per character, so a schema whose checks are mostly patterns moves accordingly (SchemaStore's elm from 289 to 89 ns a document and mimetypes from 176 to 65, against the default validator's 161 and 82).

    • An enum test on a property binds the value once instead of reading the property for every member: a 32-member enum on a required property read it 32 times. On an array of 32 items in six shapes, 2694 to 1507 ns; SchemaStore's devinit 4650 to 2454.

    • The linear-time engine steps the last character outside its loop, so the loop selects no table per character; a few percent on long strings, no change in answers.

  18. 1.48.0

    9 Oct 2026
    • ata-validator/lite is published: the Validator and the one-shot functions on the interpreted engine alone, without the code generator, for pages and workers where the size of what ships matters. It existed in the repository with its parity test since 1.19.0 and was never in the tarball or the exports map, so nothing could import it. In a Hono app bundled for Workers the runtime entry is 136 KB gzipped and the lite entry 58 KB, with the same verdicts and errors.

    • A JSR package, @ata/validator, from jsr/: it re-exports the npm package of the same version, so jsr:@ata/validator and npm:ata-validator are one engine. Published by .github/workflows/jsr.yml after the npm version is visible; tests/test_version_sync.js holds its version and specifiers to package.json.

    • CI runs the package under Deno on every change: a smoke test with dynamic code refused the way Deno Deploy refuses it (tests/deno_smoke.ts), the suite with eval and new Function blocked, and the official suite in three dialects.

  19. 1.47.0

    8 Oct 2026
    • A Hono route on Bun to its first served request through the runtime API, from benchmark/bundle: 9.1 to 7.6 ms, against 3.5 for the same app with no validation;

    • Cold start (require, compile, first validate): 8.5 to 7.0 ms; require alone 6.4 to 4.6 ms, since the error generators are read on the first error compile and the format and regex sources on the first schema that needs them;

    • Heap retained per validator that has validated, two thousand validators of a ten-key schema: 9.5 to 8.3 KB;

  20. 1.46.1

    6 Oct 2026

    Reading the note…

  21. 1.46.0

    6 Oct 2026
    • validate() on an accepted body: 37 percent less time; the verdict alone 39 percent less

    • A rejected body with every error read: 47 percent less; the realistic one 52 percent less, its verdict 59 percent less

    • The official test suite harness, every error read: draft 2020-12 70 to 49 µs per round, draft 7 30 to 29

  22. 1.45.0

    5 Oct 2026
    • The interpreted engine is faster. It answers where code generation is blocked (a Content Security Policy without unsafe-eval, some edge runtimes), it is the engine of ata-validator/lite, and it answers the first calls of a large schema. Measured against 1.44.0 on SchemaStore schemas and their sample documents:

      Where it comes from:

    • uniqueItems over arrays of objects is faster in generated code too: each item is hashed instead of turned into a canonical string, in one implementation both engines now run. A verdict on SchemaStore's SARIF samples takes 57.5 µs where it took 71.8.

    • Reading the errors of a rejected document is a little faster: the error list is created with its first error instead of grown from an empty array, and a schema that reaches its root again through a definition used once (a tree whose nodes hold trees) now gets the one-pass function that validates and collects together. On the official test suite, validating every case and reading every error, a run takes 1.05 times less time on Draft 2020-12 with the default errors and 1.05 times less on draft 7, and 1.05 and 1.11 times less with richErrors: false.

  23. 1.44.0

    5 Oct 2026
    • A schema that names something "__proto__" could accept documents it rejects. Several places copied a schema, or a map keyed by property names, by assignment, and assigning the key "__proto__" sets the copy's prototype instead of creating the property, so the constraint on it was gone from the copy. It happened for a property named "__proto__" next to a $ref that is resolved at compile time (one relative to a nested $id, for example), for a $ref pointing at such a property, and for draft-07 dependencies on such a property, which affected every engine. For example {"$id": "http://e.x/root", "$defs": {"S": {"$id": "s", "type": "string"}}, "properties": {"__proto__": {"type": "number"}, "r": {"$ref": "s"}}} accepted {"__proto__": "s"}. A schema that lists "__proto__": false to keep that key out of documents was affected the same way. Affected: measured on 1.40.1, 1.41.0 and 1.43.0; earlier versions were not checked. Every copy now writes such a key as an own property. tests/test_proto_name_rename_invariance.js checks that a schema and a document using the name answer exactly as the same pair with the name renamed, on every engine and every read, ahead-of-time modules included: 3328 documents, none different.

    • validateJSON() with richErrors: false returned the errors of a rejected document in the order generated code found them, where validate() returns them in schema order, so one document could list its errors in two orders. Both now give the same list. Reading those errors costs about 50 ns more per rejection on a small schema.

    • Faster from process start to the first answer. Measured on 20 SchemaStore schemas as API routes and on the SARIF schema as a command line tool, each run in a fresh process, median of 11, against 1.43.0 (milliseconds, an empty Node process included):

      1.43.01.44.0
      20 routes, first valid requests47.940.7
      20 routes, first errors read67.760.2
      SARIF, one rejection with its errors read90.933.4
      5 MB SARIF log through validateJSON(), valid89.157.5
      5 MB SARIF log through validateJSON(), invalid, errors read135.882.4

      Where it comes from:

  24. 1.43.0

    5 Oct 2026
    • validateJSON() and isValidJSON() could abort the whole Node process on large schemas with many references to the same definitions, such as SchemaStore's traefik and detekt. The text scanner generated for such a schema came to 0.5 to 1 MB of source in one function, and V8's Maglev compiler aborted the process (raw_hash_map<>::at) while optimizing it, after about a hundred calls, in plain JavaScript on Node 24 and 25. It was intermittent, about half of the runs on those schemas. Affected: 1.30.0 to 1.42.0, measured on the published versions. The scanner is now emitted as one function where it fits in 64 KB and otherwise split into functions of bounded size, with large definitions and members called rather than repeated; the largest scanner function on SchemaStore went from 1 MB to 64 KB. Every one of the 980 SchemaStore schemas was run in its own process, with and without the native addon, with no failure. On traefik-v2 the split scanner is 1.47 times slower than parsing the text would be; on the six other schemas that split it is 1.0 to 2.3 times faster.

    • The compile cache and the preprocess cache were unbounded. They kept every schema's text and generated functions for the life of the process: after compiling each of the 975 SchemaStore schemas once and dropping the validators, 1247 MB stayed on the heap. Both caches now keep at most 128 entries, oldest out first, and the same run keeps 572 MB. A schema compiled again after its entry was dropped is compiled again.

    • With both $defs and definitions at the root and the same name in both, generated code read #/definitions/x as $defs/x, so a document the schema accepts was rejected and one it rejects was accepted. Such a schema now runs on the interpreted engine, which resolves each pointer as written. Affected: every version from 0.6.0 to 1.42.0. None of the 980 SchemaStore schemas has both containers.

  25. 1.42.0

    4 Oct 2026
    • Reading the errors of a rejected document is faster. On the official test suite, validating every case and reading every error of each rejection, a run takes 1.4 times less time on Draft 2020-12 and 1.5 times less on draft 7 with the default errors, and 1.75 and 2.1 times less with richErrors: false. Measured against 1.41.0 in one harness, on an Apple M4 Pro with Node 25. Where it comes from:

    • More schemas run on generated code instead of the interpreted engine. Every $ref the interpreted engine can resolve (a pointer deeper than a top-level definition, an $id-relative or absolute URI, a URN, an anchor) is resolved at compile time by that engine's own resolver, and unevaluatedProperties that depends on which branches pass is evaluated at run time in generated code. Of the 980 SchemaStore schemas, 838 compile where 797 did, tsconfig.json and jsconfig.json among them. On the official suite, 326 of 384 Draft 2020-12 groups compile where 305 did, and 238 of 258 draft 7 groups where 219 did.

    • Cost of the above: the browser runtime bundle is 108.9 KB gzipped, up from 102.9 KB, and loading the package takes about 0.2 ms longer, both from the larger code generator. Modules from ata build are byte for byte the same size. A Fastify app with 10 routes reaches its first validated request 4% sooner.

  26. 1.41.0

    4 Oct 2026

    Reading the note…

  27. 1.40.1

    30 Sep 2026

    Reading the note…

  28. 1.40.0

    30 Sep 2026

    Reading the note…

  29. 1.39.2

    30 Sep 2026

    Reading the note…

  30. 1.39.1

    29 Sep 2026

    Reading the note…

  31. 1.38.0

    29 Sep 2026

    Reading the note…

  32. 1.37.1

    29 Sep 2026

    Reading the note…

  33. 1.37.0

    29 Sep 2026

    Reading the note…

  34. 1.36.1

    28 Sep 2026

    Reading the note…

  35. 1.36.0

    28 Sep 2026

    Reading the note…

  36. 1.35.0

    28 Sep 2026

    Reading the note…

  37. 1.34.0

    28 Sep 2026

    Reading the note…

  38. 1.33.3

    28 Sep 2026

    Reading the note…

  39. 1.33.2

    28 Sep 2026

    Reading the note…

  40. 1.33.1

    28 Sep 2026

    Reading the note…

  41. 1.33.0

    28 Sep 2026

    Reading the note…

  42. 1.32.3

    27 Sep 2026

    Reading the note…

  43. 1.32.2

    27 Sep 2026

    Reading the note…

  44. 1.32.1

    27 Sep 2026

    Reading the note…

  45. 1.32.0

    26 Sep 2026

    Reading the note…

  46. 1.31.3

    26 Sep 2026

    Reading the note…

  47. 1.31.2

    26 Sep 2026

    Reading the note…

  48. 1.31.1

    26 Sep 2026

    Reading the note…

  49. 1.31.0

    26 Sep 2026

    Reading the note…

  50. 1.30.3

    25 Sep 2026

    Reading the note…