Skip to content

Credit nested, referenced and dependent applicators to the unevaluated accumulators - #206

Open
wol-soft wants to merge 2 commits into
masterfrom
claude/unevaluated-annotation-crediting
Open

wol-soft wants to merge 2 commits into
masterfrom
claude/unevaluated-annotation-crediting

Conversation

@wol-soft

Copy link
Copy Markdown
Owner

unevaluatedProperties and unevaluatedItems missed several applicators that evaluate at the same instance location, and credited one that does not. Every defect here was reproduced by generating a model and exercising it, then pinned by a red test before being fixed.

Over-acceptance (invalid input was accepted)

  • A composition nested inside an object composition branch credited names declared only by a branch that lost. The branch class derived its evaluated set from a generation-time name list, which cannot express "only the branches that actually succeeded". It now computes the set at runtime from its own _compositionEvaluations, and activation recurses into nested branch schemas — their own process() pass cannot activate them, because the activation walk looks for an unevaluated keyword reachable from the schema being processed and a branch class merely sitting under an enclosing unevaluatedProperties has none of its own. This hit the most idiomatic shape there is: allOf: [{$ref: <discriminated oneOf>}].
  • unevaluatedItems: false never received the per-pass _evaluatedItemIndices reset. The render-time check matched the schema-form validator instead of the shared base class, and the false form is a sibling of it rather than a subclass. Index credits from one array value survived into the next validation, so an invalid array was accepted after any setter or populate() call.
  • A branch additionalItems with no tuple in front of it claimed every index, although the keyword has no effect without a tuple and the direct-sibling path already applied that precondition.

Over-rejection (valid input was rejected)

  • A sibling $ref contributed nothing, so the Draft 2019-09 idiom of a reference applying alongside its siblings rejected the referenced schema's own properties. The merged names travel on a dedicated Schema channel rather than being read back out of getProperties(), which cannot tell a conjunctive merge from a disjunctive branch's accessibility transfer.
  • additionalProperties / patternProperties reached two levels down were credited not at all — the mirror image of the first defect, same root cause.
  • Draft 2020-12 prefixItems was unsupported, so every index counted as unevaluated. It is now the tuple keyword, with items demoted to the trailing-entry schema when present.
  • The schema form of dependencies contributed nothing. It now credits the names it declares while its trigger property is present.

Diagnostics

  • allOf: [{$ref: ...}] is no longer reported as carrying no validation keyword. A producer-resolved branch is exempt only when it produced a schema of its own, which keeps the warning for a $ref to a file declaring nothing but definitions.

Deliberate limitations

  • A dependent subschema that reaches its properties through a composition or a $ref still cannot be credited: the dependency validator runs after the phase where unevaluatedProperties evaluates, so there is no result to read back, and a static harvest would credit branches that never ran. This now emits a generation-time warning naming the keyword instead of failing silently, and is pinned by a test that fails the day the contribution is implemented.
  • dependentSchemas, the Draft 2019-09 spelling of dependencies, remains unsupported — an absent keyword rather than a gap in a present one.
  • Empty items was investigated and is not a defect: [] and {} are indistinguishable after json_decode(..., true), and the library reads the ambiguous value as the empty schema, the only reading with a legal spelling (items: [] is invalid in every draft). A test pins that reading so it is not "corrected" later.

Verification

Full suite: 3621 tests / 10276 assertions, no regressions against the master baseline. +15 tests, +42 assertions.

One environment note for reviewers: the lock file pins the production library at 243adfc, while its own master has moved to 577dbc1. With a stale packagist cache composer update reports "nothing to modify" and six conditional-error tests fail for reasons unrelated to this branch. Advance vendor/wol-soft/php-json-schema-model-generator-production before reading a red suite.

🤖 Generated with Claude Code

…d accumulators

`unevaluatedProperties` and `unevaluatedItems` missed several applicators that evaluate at the
same instance location, and credited one that does not. Each defect was reproduced by generating
a model and exercising it before being fixed.

A composition nested inside an object composition branch was credited from a static merged name
list, which both over-accepted and over-rejected: names declared only by a branch that *lost* were
credited, while `additionalProperties`/`patternProperties` reached two levels down were credited
not at all. The branch class now derives its evaluated set at runtime from its own
`_compositionEvaluations`, and activation recurses into nested branch schemas — their own
`process()` pass cannot activate them, because the activation walk looks for an unevaluated
keyword reachable from the schema being processed and a branch class merely sitting under an
enclosing `unevaluatedProperties` has none of its own. The declared-name source moves from
`getProperties()` to the schema's own `properties` keyword: the former also holds names
transferred from anyOf/oneOf/if branches purely so the class can expose getters, plus names
materialised from `required`, none of which is evaluated unconditionally.

A sibling `$ref` contributed nothing, so the idiom that Draft 2019-09 made possible — a reference
applying alongside its siblings — over-rejected the referenced schema's own properties. The merged
names now travel on a dedicated `Schema` channel rather than being read back out of
`getProperties()`, which cannot tell a conjunctive merge from a disjunctive branch's
accessibility transfer.

`unevaluatedItems: false` never received the per-pass `_evaluatedItemIndices` reset, because the
render-time check matched the schema-form validator rather than the shared base class, and the
`false` form is a sibling of it rather than a subclass. Index credits from one array value
therefore survived into the validation of the next, silently accepting invalid arrays after any
setter or populate call.

A composition branch declaring `additionalItems` with no tuple in front of it claimed every index,
although the keyword has no effect without a tuple and the direct-sibling path already applied
that precondition.

`prefixItems` is now supported as the Draft 2020-12 spelling of tuple-form `items`, with `items`
demoted to the trailing-entry schema when it is present. The hand-off asks the draft whether it
registers the keyword: one factory instance serves every draft, and before Draft 2020-12
`prefixItems` is unknown and must be ignored, so a bare key test would silently disable `items`
there.

The schema form of `dependencies` now contributes the names it declares while its trigger property
is present. Names it reaches through a composition or a `$ref` still cannot be credited — the
dependency validator runs after the phase where `unevaluatedProperties` evaluates, and a static
harvest would credit branches that never ran — so that shortfall is reported with a
generation-time warning instead of failing silently.

Finally, `allOf: [{$ref: ...}]` is no longer reported as carrying no validation keyword. A branch
resolved by a producer is exempt only when it produced a schema of its own, which keeps the
warning for a `$ref` to a file that declares nothing but `definitions`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coveralls

coveralls commented Oct 23, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 40016051538

Coverage increased (+0.01%) to 99.33%

Details

  • Coverage increased (+0.01%) from the base build.
  • Patch coverage: 204 of 204 lines across 13 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 10291
Covered Lines: 10222
Line Coverage: 99.33%
Coverage Strength: 586.43 hits per line

💛 - Coveralls

…ansferred pattern validator writes

Closes the patch-coverage gaps the previous commit left, and fixes a dynamic-property deprecation
that covering them exposed.

Three paths had no test. A Draft 2020-12 array spelled the idiomatic way — `prefixItems` for the
tuple, `items` for everything behind it — exercised neither the stand-down that stops the `items`
pass from validating the tuple entries a second time nor the trailing-entry validator it hands off
to. A reference whose target declares `patternProperties` exercised neither the pattern side of the
evaluated-name adoption. And a chain of references through an intermediate that declares siblings
of its own exercised neither of the loops that pass on what a link adopted from the link below it.

Covering the pattern case surfaced a pre-existing bug, unrelated to the unevaluated keywords: a
base-level `$ref` transfers the referenced schema's base validators, so a PatternPropertiesValidator
can run on a class whose own JSON never spells `patternProperties` — but the field it writes was
declared only when that keyword was present. The generated class created `$_patternProperties`
dynamically, which PHP 8.2 deprecates and PHP 9 rejects. PatternPropertiesPostProcessor now follows
the validators it already iterates instead of the keyword's spelling, and returns early when none
is present so unrelated classes keep their current field set.

The chained-reference test also pinned a second pre-existing gap it cannot fix: reference merging
passes on the target's properties but not its base validators, so the innermost schema's pattern
validator never arrives and a key matching it is reported as unevaluated. The adopted *pattern*
does reach the accumulator — only the validator that must record the match is missing. That is a
reference-merging decision with a blast radius across every merged class, so the test asserts the
current outcome and will fail, pointing at the cause, once the transfer is fixed. references.rst
documents the shortfall.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants