ata-validator
1.61.3 · 29 Oct 2026MIT21.8k / weekRepositoryHomepagenpmAtom feed
1.61.3 is the newest release of 1.x.
In short
3 fixed1 changed
Release note
Fixed
validateJSON()did not return on a document that ends inside an array.validateJSON('[')was one byte, any schema, the default configuration, and it never came back. On a rejection the error carries a caret, so a map from pointer to position is built from the raw text; its array loop asked for the next element without checking that the input had ended,walkreturned without moving, and nothing else could close the array.'[}'hung the same way, because a character that begins no value leaves the position where it was. The object loop escaped only by accident: an unterminated string throws, andlib/data-position-cache.jscatches throws. It cannot catch a loop. Both container loops now stop at the end of the input, and the array loop stops when an iteration does not advance, so a truncated document still yields a map of what was there.A regression here is an infinite loop, which an ordinary test cannot report because it never reaches a failure.
tests/test_malformed_json_termination.jsruns its corpus in a child process under a watchdog, so a hang becomes a timeout and a red test. The corpus is every prefix of ten documents plus characters inserted where they cannot begin a value: 1793 inputs, 7172 verdicts. It was confirmed to fail before this change and pass after.tests/fuzz_positions.jshad never fed malformed text to this walker; it fuzzes schema positions withJSON.stringifyoutput, which is always well formed. Nothing else changes: on the same five-field schema,validate()on a valid document,validate()on an invalid one with the errors read, andvalidateJSON()on both are all within measurement noise of 1.22.0.
unevaluatedProperties: falsereported the boolean schema underneath the keyword instead of the keyword itself:keyword: 'not', emptyparams, and aninstancePathpointing at the offending member rather than the object holding it. Nothing in that error said which key was unexpected, so a caller branching onparams.unevaluatedPropertyto flag unknown keys saw nothing at all. It now reportsunevaluatedPropertieson the object, withparams.unevaluatedPropertynaming the key, one error per key.unevaluatedItems: falsehad the same shape and now reports one error for the array, withparams.limitat the first unevaluated position. Both engines were changed together andtests/test_unevaluated_error_shape.jsholds them to it. Reported by a config linter readingerr.params.unevaluatedPropertythroughata-validator/compat.verbose: trueattached onlyparentSchema. It now also attachesdata, the value the error points at, andschema, the failing keyword's own value, which is the whole set the option is supposed to carry. Withoutdataa caller had to walk the document by the instance path itself. The default error shape is unchanged: the two fields appear only whenverboseis on, including on the errorsata-validator/compatsynthesises for anifwrapper.
Changed
additionalProperties: falseno longer compiles to a comparison per declared name. It was one comparison per name per key of the document, which is quadratic in the number of properties: a 1000-property schema spent 75 µs on a document where the same schema without the keyword took 3, and doubling the properties quadrupled that. Above 128 declared names the generated code now reads a name lookup built once per compiled function; below that the comparison chain stays, because it allocates nothing and the two are the same within noise there.Cross-process medians, one variant per process, properties of type string: 250 names 8.76 µs to 5.49, 500 names 26.29 to 12.47. With no per-property work, so the membership test is all that is measured: 1000 names 77.3 µs to 18.1, 2000 names 273.6 to 38.9.
Measured and not taken:
for...inover the document instead of an indexed loop overObject.keyswas about a quarter slower at every size, even though it allocates no key array.tests/test_additional_properties_scaling.jsguards this structurally, on the emitted source: the lookup is present above the threshold, absent below it, and declared outside the function that runs per call. It asserts no timing, because a wall-clock ratio was not a guard here: the quadratic shape measured 7.7x for four times the properties on the machine the test was written on, where a CI runner measured 8.3x for the fixed code. The comparison chain short-circuits as soon as a key matches, so the ratio never separated the two. Found by a config linter whose schema is generated from a project's environment variables, so its property count is whatever the project has.