Summary
The anyOf validator accepts only a successful branch, and oneOf explicitly computes the unique successful match. In contrast, isItemEvaluated and isPropertyEvaluated inspect every anyOf/oneOf branch without checking branch validity. An annotation-producing but failed branch can therefore suppress unevaluatedItems or unevaluatedProperties enforcement even though only a non-annotating branch succeeded.
Impact: A failed branch containing items:true or additionalProperties:true marks values as evaluated; when a separate non-annotating branch satisfies anyOf/oneOf, unevaluatedItems:false or unevaluatedProperties:false is skipped and invalid instances return valid:true.
Code path
Checked against current main at commit 1cc2a3045f615ef67104f02449c6fd58afebeb6a.
src/keywords/anyOf.ts:71-80
src/keywords/oneOf.ts:76-88
src/isItemEvaluated.ts:44-58
src/isPropertyEvaluated.ts:52-66
Steps to reproduce
Validation level: current HEAD supplemental dynamic witness reproduced.
node -e 'const fs=require("fs"),vm=require("vm");vm.runInThisContext(fs.readFileSync("dist/jlib.js","utf8")); for(const combinator of ["anyOf","oneOf"]){for(const kind of ["items","properties"]){const annotatingInvalid=kind==="items"?{items:true,maxItems:0}:{additionalProperties:true,maxProperties:0}; const validNonAnnotating={type:kind==="items"?"array":"object"}; const schema={$schema:"https://json-schema.org/draft/2020-12/schema",[combinator]:[annotatingInvalid,validNonAnnotating],[kind==="items"?"unevaluatedItems":"unevaluatedProperties"]:false}; const data=kind==="items"?[1]:{x:1}; const result=jlib.compileSchema(schema).validate(data); console.log(JSON.stringify({combinator,kind,valid:result.valid,errors:result.errors.map(e=>e.code)}));}}'
Observed output:
{"combinator":"anyOf","kind":"items","valid":true,"errors":[]}
{"combinator":"anyOf","kind":"properties","valid":true,"errors":[]}
{"combinator":"oneOf","kind":"items","valid":true,"errors":[]}
{"combinator":"oneOf","kind":"properties","valid":true,"errors":[]}
Neighboring control:
node -e 'const fs=require("fs"),vm=require("vm");vm.runInThisContext(fs.readFileSync("dist/jlib.js","utf8")); for(const combinator of ["anyOf","oneOf"]){for(const kind of ["items","properties"]){const validNonAnnotating={type:kind==="items"?"array":"object"}; const schema={$schema:"https://json-schema.org/draft/2020-12/schema",[combinator]:[validNonAnnotating],[kind==="items"?"unevaluatedItems":"unevaluatedProperties"]:false}; const data=kind==="items"?[1]:{x:1}; const result=jlib.compileSchema(schema).validate(data); console.log(JSON.stringify({combinator,kind,valid:result.valid,errors:result.errors.map(e=>e.code)}));}}'
Control output:
{"combinator":"anyOf","kind":"items","valid":false,"errors":["unevaluated-items-error"]}
{"combinator":"anyOf","kind":"properties","valid":false,"errors":["unevaluated-property-error"]}
{"combinator":"oneOf","kind":"items","valid":false,"errors":["unevaluated-items-error"]}
{"combinator":"oneOf","kind":"properties","valid":false,"errors":["unevaluated-property-error"]}
Expected behavior
The documented/source-grounded contract should hold without the drift described above.
Actual behavior
Failed alternatives suppress unevaluatedItems and unevaluatedProperties, causing valid-schema false acceptance.
Existing coverage
I checked the current issue and PR lists for overlapping titles/root-cause keywords before filing this. I did not find an item covering this same root cause.
Suggested fix
Allow only successful anyOf and oneOf branches to contribute evaluated-item and evaluated-property state.
Suggested tests
- Add a regression test for the reproduction above.
- Add a neighboring control assertion so the intended non-bug path remains covered.
Submitted with Codex.
Summary
The anyOf validator accepts only a successful branch, and oneOf explicitly computes the unique successful match. In contrast, isItemEvaluated and isPropertyEvaluated inspect every anyOf/oneOf branch without checking branch validity. An annotation-producing but failed branch can therefore suppress unevaluatedItems or unevaluatedProperties enforcement even though only a non-annotating branch succeeded.
Impact: A failed branch containing items:true or additionalProperties:true marks values as evaluated; when a separate non-annotating branch satisfies anyOf/oneOf, unevaluatedItems:false or unevaluatedProperties:false is skipped and invalid instances return valid:true.
Code path
Checked against current
mainat commit1cc2a3045f615ef67104f02449c6fd58afebeb6a.src/keywords/anyOf.ts:71-80src/keywords/oneOf.ts:76-88src/isItemEvaluated.ts:44-58src/isPropertyEvaluated.ts:52-66Steps to reproduce
Validation level: current HEAD supplemental dynamic witness reproduced.
node -e 'const fs=require("fs"),vm=require("vm");vm.runInThisContext(fs.readFileSync("dist/jlib.js","utf8")); for(const combinator of ["anyOf","oneOf"]){for(const kind of ["items","properties"]){const annotatingInvalid=kind==="items"?{items:true,maxItems:0}:{additionalProperties:true,maxProperties:0}; const validNonAnnotating={type:kind==="items"?"array":"object"}; const schema={$schema:"https://json-schema.org/draft/2020-12/schema",[combinator]:[annotatingInvalid,validNonAnnotating],[kind==="items"?"unevaluatedItems":"unevaluatedProperties"]:false}; const data=kind==="items"?[1]:{x:1}; const result=jlib.compileSchema(schema).validate(data); console.log(JSON.stringify({combinator,kind,valid:result.valid,errors:result.errors.map(e=>e.code)}));}}'Observed output:
Neighboring control:
node -e 'const fs=require("fs"),vm=require("vm");vm.runInThisContext(fs.readFileSync("dist/jlib.js","utf8")); for(const combinator of ["anyOf","oneOf"]){for(const kind of ["items","properties"]){const validNonAnnotating={type:kind==="items"?"array":"object"}; const schema={$schema:"https://json-schema.org/draft/2020-12/schema",[combinator]:[validNonAnnotating],[kind==="items"?"unevaluatedItems":"unevaluatedProperties"]:false}; const data=kind==="items"?[1]:{x:1}; const result=jlib.compileSchema(schema).validate(data); console.log(JSON.stringify({combinator,kind,valid:result.valid,errors:result.errors.map(e=>e.code)}));}}'Control output:
Expected behavior
The documented/source-grounded contract should hold without the drift described above.
Actual behavior
Failed alternatives suppress unevaluatedItems and unevaluatedProperties, causing valid-schema false acceptance.
Existing coverage
I checked the current issue and PR lists for overlapping titles/root-cause keywords before filing this. I did not find an item covering this same root cause.
Suggested fix
Allow only successful anyOf and oneOf branches to contribute evaluated-item and evaluated-property state.
Suggested tests
Submitted with Codex.