ata-validator
1.61.3 · 29 Oct 2026MIT21.8k / weekRepositoryHomepagenpmAtom feed
1.61.3 is the newest release of 1.x.
In short
3 faster6 fixed
Release note
Fixed
The verdict methods answer the same question as
validate()again. WithcoerceTypes,removeAdditionalor a schemadefaultin play,validate()ran the preprocess pass andisValidObject(),isValidJSON()andvalidateJSON()did not, so the same validator answeredtruefrom one andfalsefrom another for the same document:validate({ age: '26' })accepted whereisValidObject({ age: '26' })rejected. Every path now runs the same pass. Verdict methods on a validator configured this way rewrite the input in place, asvalidate()already did, and the cost of the correction is 0.5 ns onisValidObjectand 2.7 ns onisValidJSON, measured interleaved; validators without those options are unchanged.tests/test_verdict_preprocess.jsholds all four methods to the same answers.The native engine's error codes no longer reach callers untranslated. A type failure answered by the addon came back as
code: 3with nokeywordand adocUrlpointing at a page that does not exist, while the same failure from the JavaScript engines came back asATA1001; both now report the documented code, keyword and link.tests/test_native_error_codes.jsholds the table against the enum ininclude/ata.h, so the two cannot drift apart silently.The error generator declined self-referencing schemas by emitting nothing for the reference, which accepted whatever that reference guarded:
{ properties: { foo: { $ref: "#" } }, additionalProperties: false }accepted{ foo: { bar: false } }on that path. It now declines the schema outright and the validator falls back to an engine that answers it correctly. The entry-point agreement test covers the shape.ipv6gave two different answers depending on which engine ran it, and neither was right. The compiled path refused an IPv4-mapped address such as::ffff:192.168.1.1, the interpreted path accepted::ffff:1.2.3.4.5, and both accepted a group of five hex digits like12345::1. One implementation now answers for every engine, following RFC 4291, checked against Node's ownnet.isIPv6and the suite's corpus.date-timerefuses dates that do not exist. The check ran a regular expression for the shape and then handed the string toDate.parse, which rolls an out-of-range day into the next month, so2026-02-30T00:00:00Z,2026-02-29T00:00:00Zand2026-12-31T24:00:00Zwere all accepted. The month, the day count for that month in that year, the clock and the offset are now checked directly, per RFC 3339. Schemas that relied on the old leniency will see those values rejected.Data that points back at itself no longer exhausts the stack. A document with a cycle, which JSON text cannot express but an in-memory object graph can, threw
RangeError: Maximum call stack size exceededon the compiled path while the interpreted engine settled on an answer, so the two engines disagreed. Both now follow the same rule: a value already being checked against a schema is a fixed point and counts as satisfied, and a cycle no longer hides a real violation elsewhere in the document. Validation runs a fast pass that only counts depth and a guarded pass that runs when that depth is exceeded, so ordinary documents pay one integer operation per recursive call. Measured interleaved on a self-referencing schema: a four-node document 22.2 to 30.6 ns, a 200-node document 2077 to 1178 ns, non-recursive schemas unchanged at 3.8 ns.tests/test_cyclic_input.jsholds all three engines to the same answers.
Performance
ipv6andhostnameread the string once as well: 54.7 to 29.6 ns and 45.5 to 28.2 ns, interleaved medians.ipv6no longer allocates two arrays per check;hostnamekeeps the answers of the expression it replaces, fuzzed over 300k strings with 0 mismatches.date-timereads the string once, with no regular expression, no date object and no allocation: 95.0 to 39.4 ns on a valid value with aZ, 103.8 to 45.3 ns with a numeric offset, interleaved medians. Fuzzed against a reference that spells out RFC 3339, with 0 mismatches over 300k strings;tests/test_formats_single_pass.jskeeps both the predicate and the generated form on it.A constructed Validator is roughly three times smaller on the heap until it is used. The public methods and the Standard Schema entry moved from per-instance closures built in the constructor to memoized prototype accessors, and the JSON position cache is only allocated when the JSON text path first needs it. Measured per instance on a 10-key object schema, double-gc deltas over 2000 instances: 1.61 KB to 0.43 KB with a shared schema object, 2.33 KB to 1.12 KB when each instance owns its schema, 3.93 KB to 3.30 KB once compiled and used. Construction alone went from 1504 to 855 ns; construction plus first validate pays about 0.9 microseconds more, once, because the compile step's method assignments now go through a defining setter. The hot validate() path is unchanged, measured interleaved. Detached method references (
const f = v.validate) still work;tests/test_lazy_instance.jspins the shape.