Repository navigation
validate.mjs: boundSchema depth limit corrupts schemas with three or more allOf levels #134
Description
Activity
Could not reproduce on current
mainwith the three schemas as written: the run reportsRT-LOSSYfor the missing@context, notexamples must be array. The guard is stillboundSchema(root, maxDepth = 6)atscripts/validate.mjs:144, but line 180 cuts onminDepth.get(node) ?? 0- the shallowest depth a shared node was reached at. Validating a directory that also containsA.schema.jsonreaches that subtree at depth 1 from A itself, so the memoised minimum stays under the limit. Worth confirming whether the original run validatedC.schema.jsonalone, which would not have that shortcut.Independently of that: the Python port does not have this failure mode. Same
DEFAULT_MAX_DEPTH = 6, andexamplesandenumsurvive intact through six levels of inheritance:B (2 levels): examples=[1] enum=[1, 2] intact C (3 levels): examples=[1] enum=[1, 2] intact D (4 levels): examples=[1] enum=[1, 2] intact E (5 levels): examples=[1] enum=[1, 2] intact F (6 levels): examples=[1] enum=[1, 2] intactSo if the corruption is real under some invocation, it is
validate.mjs-specific and the migration retires it rather than needing a fix in both places. Givenvalidate.mjsis being frozen and extracted tooold-js, the question is whether it is worth fixing there at all, or whether this closes when the swap lands.What would settle it: the exact command from the original run, and whether it targeted the single file or the directory.
Not reproducible in
oold-python, which now does the validation:bound_schemacounts instance-nesting depth, not raw object nesting, so a subclass chain does not consume the budget. A six-level chain (L0..L5) withexampleson the base passes:156 ok, 0 failedacross 6 targets.That is the first of the two fixes suggested here, already in place on the Python side.
scripts/validate.mjsstill has it. It is frozen and only runs as the parity reference (make validate-reference), so this travels with the extraction tooold-js(#91).Correction to my earlier comment: I said
scripts/validate.mjsstill had this. It does not, and had not when this was filed.boundSchemacounts instance depth, not raw object nesting -INSTANCE_KEYWORDS/INSTANCE_MAP_KEYWORDS, whereallOfand$refare depth-neutral. That is the first of the two fixes proposed here, and it landed in #103 on 2026-07-29, two weeks before this issue.Measured on the exact reproduction above, and on a six-level chain:
node src/validate.mjs /tmp/i134 --meta .../meta -> no GEN-ERROR, no "must be array" node src/validate.mjs /tmp/depth5 --meta .../meta -> 31/31 checks passedThe path in the "Cause" section -
allOf(1)[0](2)allOf(3)[0](4)properties(5)v(6)examples(7) - is raw nesting. Under the accounting the code actually uses, onlypropertiescosts budget, sovsits at depth 1 and nothing is cut. So either the reproduction was run against a checkout predating #103, or there is a variant that still fails and the repro above is not it.The code has since moved to OO-LD/oold-js (#156). Worth closing unless you have a case that still reproduces there, in which case it should be reopened on that repository.
The
boundSchemadepth guard inscripts/validate.mjsreplaces any node deeper thanmaxDepth = 6with{}. In a three-level inheritance chain that truncation lands inside a keyword whose value has a required type, so ajv rejects a schema that is actually valid.Reproduction
Three schemas, each extending the previous one, with
exampleson a property of the base:Cause
After dereferencing, the path to the base annotation is
allOf(1)[0](2)allOf(3)[0](4)properties(5)v(6)examples(7).At depth 7
walk()returns{}, soexamples: [1]becomesexamples: {}, and ajv fails it asmust be array. Two levels of inheritance stay under the limit, three do not, which is why this has not shown up on the examples in this repository.Why it matters
It is not the annotation that is at fault. Any keyword requiring a non-object value (
examples,enum,required,x-enum-varnames) becomes{}at that depth, so the failure mode is a valid schema reported as invalid, and the message points at the schema rather than at the depth guard. Reference-schema libraries reach three levels quickly: a base value type, a per-quantity restriction, and a specialisation of that.Possible fixes
$ref/allOfhops rather than raw object nesting, since the guard exists for reference cycles, not for deep-but-finite documents.maxDepthand rely on the existing cycle detection (path.has(node)), which already terminates cycles on its own.A workaround for schema authors is to move
examplesto the schema root, where it is a whole example instance and sits at depth 1, but that only avoids the symptom for one keyword.Found while building three-level inheritance (
QuantityValue->Length->Diameter) in the reference schemas.