Conversation
Introduces the scaffolding needed for unevaluatedProperties/unevaluatedItems tracking without changing any existing validation behaviour: - AbstractComposedPropertyValidator: add trackEvaluation flag (getter/setter) so later phases can gate _compositionEvaluations cache emission per validator. - Schema: add postCompositionValidators list (addPostCompositionValidator / getPostCompositionValidators) for validators that must run after all composition validators complete. - Model.phptpl: extend executeBaseValidators with a third bucket that iterates schema.getPostCompositionValidators(); update both guard conditions to trigger the method when either the base-validator list or the post-composition list is non-empty. Template variables are not used for the new bucket — the schema object is already in context and the method is called directly. - UnevaluatedPropertiesPostProcessor (new internal post processor): activation walk via DFS over composition branches and nested schemas, breaking cycles through a file+pointer seen-set; sets trackEvaluation on matching validators and emits the _compositionEvaluations cache property when activation triggers. - ModelGenerator: register UnevaluatedPropertiesPostProcessor immediately after CompositionValidationPostProcessor. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts: # tests/AbstractPHPModelGeneratorTestCase.php # tests/PostProcessor/EnumPostProcessorTest.php
When unevaluatedProperties or unevaluatedItems is reachable from a schema, each composition validator now records the per-branch evaluation outcome in a typed _compositionEvaluations slot: null when the branch failed, true when the branch evaluated every property (non-false additionalProperties), an array of evaluated property names for an inline branch, or the instantiated branch object for a nested-schema branch. The shared evaluation logic moved to the production-library CompositionEvaluationTrait so the slot writes don't duplicate the same loop across every generated class. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three improvements layered on the Phase 2 tracking infrastructure: Pre-decode patternProperties patterns at generation time. CompositionPropertyDecorator now exposes getBranchDeclaredPropertyNamesPhpLiteral() and getBranchPatternPropertyPatternsPhpLiteral() returning var_export'd PHP array literals ready for direct template embedding. Templates drop the per-name quoting foreach loop, and the trait no longer calls base64_decode on every preg_match. var_export also handles all string escaping, so property names containing single quotes or backslashes no longer produce broken generated code. Skip the tracking write on a cache hit. ComposedItem.phptpl initializes $shouldTrack = true per branch iteration; the mutable-validator cache-hit branch sets it to false so the success-tracking block is bypassed and the slot value from the previous run is preserved. On a cache hit $value still holds the pre-validator array rather than the nested-schema object, so the old unconditional write would overwrite a valid object slot with null. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# Conflicts: # src/Model/Validator/ConditionalPropertyValidator.php # src/Templates/Model.phptpl # tests/Basic/PhpAttributeTest.php
Adds the runtime validator for `unevaluatedProperties: false` and `unevaluatedProperties: <schema>` on top of the existing composition evaluation tracking. The validator iterates the model keys left over after subtracting the local `properties`/`patternProperties` set plus the contributions of every successful sibling composition branch recorded in `_compositionEvaluations`. The factory skips emitting any validator when the same schema declares a non-false `additionalProperties`: that keyword already claims every remaining key, so unevaluatedProperties would be a no-op. The cross-schema modification needed for nested branch classes (getEvaluatedProperties) is now applied during `process()`. To keep that visible at render time, `RenderQueue::execute` runs every job's `process()` pass before any `render()` begins. The shared evaluated/unevaluated computation lives on `CompositionEvaluationTrait` in the production library; both validator templates collapse to a single trait-method call. Adds `RenderHelper::varExportPcrePatterns()` to wrap raw patternProperties keys with `/`-delimiters and escape embedded `/`. The existing patternProperties consumers (`AdditionalPropertiesValidator`, `AdditionalPropertiesPostProcessor`, `CompositionPropertyDecorator`) are migrated to it, fixing a latent crash on patterns containing slashes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
# Conflicts: # tests/PostProcessor/AdditionalPropertiesAccessorPostProcessorTest.php # tests/PostProcessor/PatternPropertiesAccessorPostProcessorTest.php
# Conflicts: # src/Model/Validator/AbstractComposedPropertyValidator.php # src/Model/Validator/ComposedPropertyValidator.php # src/Templates/Validator/ComposedItem.phptpl
The dev-jsonSchemaDraft2019 production library widened MaxItemsException to require an actual count alongside the maxItems limit. The items: false branch of ItemsValidatorFactory still passed only the maxItems literal (via [0]), producing a 3-arg call that broke against the 4-arg constructor. Mirror the working pattern from MaxItems / MinItems factories: define $count inside the validation expression and pass it as the fourth constructor argument by reference. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduces UnevaluatedPropertiesAccessorPostProcessor, an opt-in post
processor that exposes the keys claimed by `unevaluatedProperties` via a
single getter on the model: `$model->unevaluatedProperties()` returns either
the bare production-library accessor (untyped schema) or a generated
companion class `{ModelName}UnevaluatedProperties` with narrowed get/set/
getAll/remove signatures (typed schema). Private mutator shims
`_setUnevaluatedProperty` / `_removeUnevaluatedProperty` host the runtime
guard and per-call validation; the accessor's closures bind to them via
first-class callable syntax. The shim guard rejects keys that match a
directly-declared property name, a local `patternProperties` pattern, or
any property name/pattern declared inside a composition branch (allOf /
anyOf / oneOf / if / then / else, walked recursively) — those keys belong
to a different contract and routing them through the unevaluated bucket
would silently bypass the typed validator.
Accessor emission honours the keyword's reachability at this schema level:
when `additionalProperties: false` rejects every extra or
`additionalProperties: {schema}` claims every extra, the unevaluated bucket
is unreachable and nothing is emitted (no backing field, accessor method,
shims, companion, or collect flag) — the validator continues to run as a
pure assertion.
UnevaluatedPropertiesValidator gains a `setCollectUnevaluatedProperties`
flag (mirroring the additional-properties pattern) so the validator writes
into `$this->_unevaluatedProperties` after per-key validation succeeds,
with a rollback on whole-validator failure to preserve the prior bucket
state. SerializableTrait in the production library learns to flatten
`_unevaluatedProperties` back into `toArray()` / `toJSON()` output via a
new `_serializeUnevaluatedProperties()` method.
Populate.phptpl's hardcoded rollback / accessor-cache property lists are
replaced with two registries on Schema (`addRollbackProperty`,
`addAccessorCacheProperty`). Existing internal post processors register
their fields into the registries; the new accessor registers
`_unevaluatedProperties` and `_unevaluatedPropertiesAccessor`. The change
is a no-op for any schema that does not opt into the unevaluated accessor.
Test suite (UnevaluatedPropertiesAccessorPostProcessorTest, 15 tests, all
Drafts 2019-09 and 2020-12): typed round-trip; constraint-violation
rejection in direct-exception mode with rollback verification; runtime
guards (declared property, composition-branch property); companion-class
naming and method surface; immutable read-only variant; serialization
round-trip; dead-code suppression; collect-errors-mode setter isolation
(proves the per-call error-registry reset is load-bearing); coexistence
matrix asserting the additionalProperties × unevaluatedProperties emission
policy when both accessor post processors are configured simultaneously.
CLAUDE.md gains an expanded patterns list and recovery procedure for the
"no implementation-plan references in code" rule so that section numbers,
decision identifiers, and similar artifacts of working notes can be
caught before they land.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Splits _executeBaseValidators into two phase methods so the constructor, setters, and populate can invoke whichever phases they need against whichever input they need. The schema-level validators that depend on the merged state of every property — currently just unevaluatedProperties — now live in _executePostCompositionValidators, and the property-level plus composition validators stay in _executeBaseValidators. Constructor calls each method in turn against the raw input. Setters call _executePostCompositionValidators against a candidate raw input (current state with the new value plugged in) before commit, so a setter that flips a oneOf discriminator and would orphan a key from the previous branch's contract is rejected before the model state changes. populate() runs the delta check via _executeBaseValidators (which no longer includes the post-composition phase) and then runs _executePostCompositionValidators against the would-be merged state; failure rolls every committed property field back and leaves _rawModelDataInput untouched. The accessor's _setUnevaluatedProperty shim now calls _executePostCompositionValidators directly: a single dynamic key only ever needs the post-composition phase, not the schema's full base + composition chain. Each call resets its own error registry so a caller catching a failed set() can continue to use the model. Test class UnevaluatedPropertiesMutabilityTest covers the setter rollback path on each composition shape (oneOf, anyOf overlap, anyOf sole-coverer, if/then/else), the no-op early-return path, populate's merged-state rollback, the collect-errors mode collation of property-validator failures together with unevaluated-properties failures in a single registry, and the cross-state revalidation reading through nested-branch class instances. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wire cross-state revalidation into _setAdditionalProperty and
_removeAdditionalProperty so dynamic-key mutations see the post-mutation
state. Set runs base validators followed by _executePostCompositionValidators
against a candidate raw input. Remove additionally re-runs every composition
validator against the post-removal raw input so _compositionEvaluations
reflects the new branch outcomes, then runs the post-composition phase. Both
share a single _errorRegistry across phases in collect-errors mode and
snapshot/rollback _additionalProperties, _rawModelDataInput,
_compositionEvaluations, and _propertyValidationState on rejection.
Fix the PHP 8.4 dynamic-property deprecation triggered when a composition
branch declares additionalProperties:{schema}: setupBranchDefaultHelpers no
longer propagates internal properties from a branch's nested schema into
allBranchDefaultAttributeMap. Internal properties have no getter and the
parent never declares the corresponding field, so the propagation loop's
$this->$attr = ... writes were creating dynamic properties on the parent.
Emit a generation-time warning when unevaluatedProperties is suppressed
because a sibling additionalProperties is non-false (the dead-code cell from
the §4.1 matrix). Gate the warning on isOutputEnabled() for parity with the
rest of the warning channel; gate the previously-unconditional echo in
EnumPostProcessor::filterValuesByDeclaredType the same way; let the test
infrastructure respect the caller's outputEnabled so warning-assertion tests
can opt in.
Test coverage added for §5.1.20 (required + unevaluated error precedence in
both direct and collect modes), §5.1.A1 (defaults are not part of the
evaluated set), §5.1.A3/A4 (branch-level additionalProperties per-key
validity), §5.1.25 (pattern + unevaluated bucket coexistence), §5.1.27
(empty allOf), §5.1.M33-34 (additionalProperties accessor under outer
unevaluatedProperties), §5.1.M37 (remove clears backing field plus raw
input), §5.1.M42 (claimed-via-unevaluated key persists after sibling later
covers), the dead-code suppression warning, the Set cross-state revalidation
rejection path, and the dual-error collect-mode aggregation for both Set
(pattern + unevaluated, paired with the per-key validity fix on the
production library) and Remove (min-property + unevaluated via composition
revalidation flipping an anyOf branch). Both dual-error tests assert the
full ErrorRegistryException message via heredoc.
CLAUDE.md adds a rule requiring multi-line exception-message assertions to
use heredoc inlined into the assertSame call.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A composition branch with its own unevaluatedProperties:{schema} now
contributes the keys it evaluates against that schema to an enclosing
unevaluatedProperties through the same nested-class read-through the trait
already uses for declared properties.
UnevaluatedPropertiesPostProcessor adds an internal _evaluatedPropertyKeys
field to any schema carrying the schema-form UnevaluatedPropertiesValidator.
UnevaluatedProperties.phptpl writes the key into this field on per-key
validation success, with snapshot/rollback mirroring the existing
_unevaluatedProperties discipline.
Nested branch classes gain an _getEvaluatedProperties() method (underscore
prefix + #[Internal]) returning the union of declared properties present in
the raw input and keys recorded in _evaluatedPropertyKeys. The underscore
sidesteps a name collision with a user-declared evaluatedProperties
property's auto-generated getter and the attribute documents the role.
testNestedUnevaluatedInBranchPropagatesClaimsToOuter runs in collect-errors
mode so a single ErrorRegistryException carries the full chain: the outer
allOf failure, the nested class's invalid-unevaluated-property cause, and
the outer unevaluatedProperties: false rejecting the orphaned foo and bar.
The assertion uses heredoc to compare the full message, with an inline
comment explaining why foo appears as an orphan despite passing the
branch's local properties.foo validator — failed composition branches
contribute no annotations to the outer accumulator under JSON Schema
2019-09's applicator rules.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduces UnevaluatedItemsValidator and NoUnevaluatedItemsValidator on the
array side, mirroring the property-side flavours. The validator runs at
priority 101 on the array property's validator chain (after composition at
100), wrapping its check in is_array($value) so it short-circuits cleanly
for union types like ["array", "null"]. Both flavours render through a
new collectUnevaluatedIndices() trait method that reads
\$this->_evaluatedItemIndices defensively (the field arrives in a later
stage) and walks \$this->_compositionEvaluations slots filtered by
'kind' => 'array' so the property-side and array-side rebuilds cannot
contaminate each other on mixed-type oneOf.
UnevaluatedItemsValidatorFactory skips emission for absent and true
keywords, throws a SchemaException for non-bool/non-object values, and
warns through the existing echo channel for the three array-side dead-code
shapes — items:false, items:{schema}, and additionalItems:false with tuple
items — that make the keyword unreachable. The property-side factory gains
the same SchemaException guard for non-bool/non-object unevaluatedProperties
values (an oversight that the array-side test surfaced).
Draft_2019_09 registers the factory on the array type. The unevaluated post
processor's needsActivation walk now inspects each property's own JSON
before recursing into its nested schema, so a schema like
{type: object, properties: {tags: {type: array, unevaluatedItems: false}}}
activates the outer object's evaluation tracking (array properties carry
no nested schema, so the previous loop missed them).
The production library gains UnevaluatedItemsException and
InvalidUnevaluatedItemsException. The false-form message reports indices
with the # prefix used by the rest of the array-side family
(InvalidItemException, InvalidTupleException); the schema-form wrapper
preserves the per-index inner exceptions verbatim, so consumers catching
the wrapper can still surface the underlying InvalidTypeException reason.
CLAUDE.md adds two enforcement rules. "Always review the diff against the
repo rules before staging" prescribes a grep recipe for plan/stage/
phase/section references and applies symmetrically to coordinated
production-library checkouts. "Pre-existing rule violations in touched
files" requires sweeping every visible CLAUDE.md violation in any file
edited as part of a task, with one escape hatch when cleanup would balloon
into an unrelated refactor.
UnevaluatedItemsValidatorTest covers the bare false and schema forms
(per-index # notation), uniqueItems-precedence ordering, the four dead-code
warning shapes via expectOutputRegex, and the SchemaException path for
non-bool/non-object values on both keywords. The schema-form test asserts
the full multi-line exception message via heredoc using two failing indices
and pins getInvalidItems() to confirm each failing index's
InvalidTypeException survives the wrapper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Property-level allOf/anyOf/oneOf/if-then-else compositions on array
properties now contribute their successful branches' claimed indices to a
sibling unevaluatedItems validator. The bridge is a single transient
_compositionAnnotated field indexed by slot key, written wholesale at end
of each composition IIFE, read by the unevaluatedItems template via the
prod-lib trait method.
The activation walk in UnevaluatedPropertiesPostProcessor descends
recursively through composition branches' wrapped properties (so nested
compositions get tracking too) and flags any ArrayContains validators
inside tracked branches with trackBranchMatches, which makes the contains
template export a per-index match map captured by the surrounding
composition body.
Sharp edges fixed along the way:
* ExtractedMethodValidator method-name hash mixed in spl_object_hash so
two compositions on the same property no longer collide into one
method body
* Activation walk reads source validators via getWrappedProperty()
because PropertyProxy::getOrderedValidators() returns fresh
withProperty() clones on every call — flag mutations on the clones
never reached the rendered instance
* Composition IIFE leaves $value untouched when slotKey is set so a
failing composition does not null the caller's array value and
suppress the downstream unevaluatedItems check in collectErrors mode
* activateArrayComposition has a hash-keyed cycle guard so a self-
referencing schema does not recurse indefinitely through $ref-induced
composition cycles
Requires php-micro-template 1.11.0 for the {# ... #} template-time
comment syntax used by ComposedItem.phptpl. CLAUDE.md adds two rules
surfaced by the work: never prefix local variables with underscore (the
prefix is reserved for class members); when a bug is found, write a
reproducing test before fixing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The post processor previously added `_compositionEvaluations` and `_compositionAnnotated` unconditionally on every activation-triggering schema, justified by a comment claiming the trait reads them on no-composition schemas and PHP 8.2 would deprecate the undeclared- property access. Both halves of that justification were wrong: the trait's reads go through `?? []`, which does not trigger the dynamic- property deprecation, and the read sites only execute when the caller passes composition slot keys — empty on no-composition schemas. The fields were padding every activation-triggering generated class with two unused arrays. Each field now follows the precedent set by `addEvaluatedPropertyKeysField` and is declared only when something will write to it. The two activation passes have been extracted into `activateSchemaLevelTracking` (handles the root-level composition activation and `_compositionEvaluations`) and `activateArrayPropertyTracking` (handles the array-property walk plus `_compositionAnnotated` and `_evaluatedItemIndices`); `process()` now just orchestrates them. - `_compositionEvaluations` — only when at least one `AbstractComposedPropertyValidator` at the schema level had evaluation tracking enabled. - `_compositionAnnotated` — only when the array-property activation walk actually enabled tracking on a property-level composition. - `_evaluatedItemIndices` — new field, declared only when at least one array property carries `unevaluatedItems` (the future write site is in `UnevaluatedItems.phptpl`; the field is empty until then). `UnevaluatedItemsMutabilityTest` exercises what Stage 5.3's transient- field design relies on without any new validator wiring: the whole-array setter rebuilds composition annotations against the candidate array and re-runs `unevaluatedItems` against it (rejecting an array whose tail index no branch claims, preserving pre-setter state on rejection); `populate()` with array data of a different length flows through `PopulatePostProcessor`'s revalidation hook and behaves the same. Both tests use `setCollectErrors(false)` so the raw `UnevaluatedItemsException` surfaces directly, mirroring the existing `UnevaluatedPropertiesMutability Test` pattern. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The unevaluatedItems template now writes _evaluatedItemIndices per successfully-validated index and snapshots/restores the field around the IIFE so a nested unevaluatedItems inside a composition branch contributes its claims to the enclosing accumulator on success and leaves no partial writes behind on failure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Verifies the activation walk already handles $ref-resolved schemas through
the property tree it traverses (via getNestedSchema on properties and
composition branches). No generator changes required.
Coverage:
- unevaluatedProperties expressed as {$ref: "#/$defs/foo"}
- $ref-resolved allOf branch contributes annotations to outer accumulator
- Recursive self-$ref terminates and enforces unevaluatedProperties at
every nesting depth (validated with full-message assertions unwrapping
through NestedObjectException::getNestedException)
- External $ref to ExternalSchema placeholder used as composition branch:
markTestIncomplete, blocked by pre-existing composition-processor
requirement that every composed branch surface a nested schema (same
limitation as the array-side self-referencing test)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…uated-properties # Conflicts: # src/Model/Validator/Factory/Arrays/ItemsValidatorFactory.php
The merge from jsonSchemaDraft2019 landed pointer stamping on every validator factory but surfaced two follow-up gaps in the unevaluated work. ConditionalPropertyValidator did not update templateValues['compositionValidator'] at render time. withJsonPointer() clones the validator, but the clone's template still pointed at the pre-clone original whose evaluationTrackingEnabled flag is false. Every if/then/else composition consequently emitted no _compositionEvaluations writes, so an if-branch that claimed a property left the outer unevaluatedProperties accumulator empty and the key was reported as unevaluated. Late-bind in getCheck() the same way ComposedPropertyValidator already does. Neither unevaluated factory (object nor array) chained ->withJsonPointer() on the emitted validator, so UnevaluatedItems / UnevaluatedProperties / InvalidUnevaluated* exceptions carried an empty pointer. SetUnevaluatedProperty.phptpl's two RegularPropertyAsUnevaluated throw-sites emitted no pointer at all. Wire real pointers through three shapes: - Factory-level: /path/to/property/unevaluatedItems and /unevaluatedProperties, matching the sibling stamping in additional/pattern factories. - Object-property harvest (root declaration): resolved via each property's synthesized #[JsonPointer] attribute, matching how AdditionalPropertiesAccessorPostProcessor resolves it. - Composition-branch harvest (inline branch declaration): walked recursively with per-branch pointers, so a property declared inside /allOf/0 gets /allOf/0/properties/<name>. Pattern-based entries carry their /patternProperties/<encoded-regex> pointer verbatim. Pointer utilities extracted to Utils\JsonSchema. encodePointer and decodePointer moved off the Model\SchemaDefinition\JsonSchema data class where they had been static bystanders; resolvePrimaryJsonPointer extracted from the two accessor post processors that had duplicated it. All call sites aliased-import as JsonSchemaUtil to avoid a name clash with the model class. RenderHelper::varExportPcrePatternMap added so a template can foreach over a regex => pointer map when the pattern-guard needs both fields at runtime. Pointer assertions added to the acceptance tests that already caught the message text and payload. The pattern-focused accessor test's fixture regex changed from `^x_` to `^s/~` so the emitted pointer becomes `/patternProperties/^s~1~0`, exercising both RFC 6901 replacements and their ordering in a single assertion. The three RegularPropertyAsUnevaluatedPropertyException catches now also assert the full "Couldn't add regular property … as unevaluated property to object …" message. Requires the coordinating production-library commit 7bfa040 (jsonPointer arg added to UnevaluatedItems / UnevaluatedProperties / RegularPropertyAsUnevaluatedProperty / MinContains / MaxContains). Push that commit and \`composer update\` to lock the new SHA. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
UnevaluatedPropertiesValidatorFactory now consolidates all four
sibling shapes that leave the unevaluatedProperties bucket
permanently empty into a single deadCodeReason() helper:
- additionalProperties: true — every extra flows to the model but
the accumulator does not credit it, so the validator would still
fire and defeat the intent of `additionalProperties: true`.
- additionalProperties: {schema} — every extra is claimed and
validated by additionalProperties; the unevaluated set is empty.
- additionalProperties: false — every extra is rejected before the
post-composition phase, so the unevaluated validator never runs.
- denyAdditionalProperties() generator flag with additionalProperties
absent — synthesises the same false shape at configuration time.
Each cell emits a distinct warning via the existing echo channel
so build-output greps can identify the specific dead shape, and
skips validator emission entirely.
Tests added:
- Object-side dead-code data provider covering all four shapes plus
the denyAdditionalProperties() variant. Rows pass the
GeneratorConfiguration directly (setOutputEnabled(true) on the
base, chained with setDenyAdditionalProperties(true) on the deny
row) so the test signature drops the closure/fallback boilerplate.
- testAdditionalFalseRejectsExtrasEvenWhenUnevaluatedSchemaWouldAccept
proves the factory suppressed the validator: an extra that would
satisfy the unevaluated integer schema is still rejected because
additionalProperties: false runs first.
- testContradictoryInnerSchemaThrowsSchemaExceptionPointingAtFile
pins that an unevaluatedProperties schema with contradictory allOf
types surfaces via the existing "conflicting types" SchemaException
path, with the file identifier preserved.
- testPropertyNamesRejectionPrecedesUnevaluated data-provider variant
covering both direct-exception and error-collection modes. The
registry message pin includes the base-phase propertyNames block
followed by the post-composition InvalidUnevaluatedPropertiesException
block; template uses `{className}` + str_replace for interpolation.
- testDependentSchemasContributionCreditsDependentPropertiesToAccumulator
is skipped (dependentSchemas applicator not yet implemented) but
asserts both the intended success path (kind + covered `extra`)
and the failure path (kind + non-covered `stray`) so removing the
skip line makes the applicator's contract live.
- Array-side testContainsWithMinContainsZeroCreditsMatchedIndicesOnly
and testOneOfBranchesOfDifferentTupleLengthsControlEvaluatedSet
cover contains + minContains: 0 and mixed-tuple-length composition.
Object-side dead-code fixtures cover all six sibling shapes;
denyAdditional variant paired with the config row.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Coverage Report for CI Build 31972611204Coverage increased (+0.03%) to 98.906%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Adds resolveNestedClassName() to the abstract test case so generated class names can be discovered and interpolated into expected exception messages instead of matched via \S+/\w+ regex. Replaces the regex-based assertions across four test files with literal-string assertions, tightening what "the message must contain" checks were previously letting through. Also adds two confirmatory fixtures and tests for patternProperties / additionalProperties value-subschemas that declare their own unevaluatedProperties: false, verifying that the inner class self-activates through its own post-processing pass without needing the parent's activation walk to recurse into these sibling schemas. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the docs Phase 8 work (unevaluatedProperties/unevaluatedItems sections in object.rst/array.rst, per-composition propagation notes, a dedicated accessor post-processor page) plus the pre-existing sphinx warnings we hit along the way. While adding filter-companion tests, two transforming-filter regressions on the unevaluated side surfaced: the set() shim rejected already-transformed values because the pre-transform type-check was not pass-through-wired, and toArray() dropped transformed values because no filter-aware serializer override was emitted. TransformingFilterOutputTypePostProcessor and SerializationPostProcessor now iterate post-composition validators the same way they already iterate base validators, and a new UnevaluatedPropertiesSerializer template mirrors the additional-side serializer. Also updates the additional-properties accessor doc to match its actual companion signatures and drops one unused import in FilterProcessor. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Every behavioural case the reflection test asserted (allOf key contribution, anyOf per-branch success/fail, not-branch rollback, if/then/else slot layout) is covered end-to-end by UnevaluatedPropertiesValidatorTest via observable acceptance and rejection assertions. Inspecting the internal _compositionEvaluations cache added no coverage the validator tests do not already provide and coupled the test to a private field. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The if-null-nested-schema block in generateValidatorPropertyMap() cannot execute for the composition validators that method iterates. SchemaProcessor::transferComposedPropertiesToSchema() throws SchemaException for any schema-level composed property that lacks a nested schema, and inheritPropertyType() forces branches to adopt the parent's object type so PropertyFactory routes every inline branch through createObjectProperty() and sets the nested schema. Setter-side revalidation for inline oneOf, anyOf, and if-then-else discriminators is already covered by the strict path iterating getNestedSchema()->getProperties(), asserted end-to-end by the mutability tests exercising kind and mode discriminator flips. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ten deliberately failing tests pin the spec-correct behaviour for the defects surfaced by the review of the unevaluatedProperties / unevaluatedItems implementation. Each test turns green when its fix lands: - AutoDetectedDraftSubschemaTest (new, carrying no ApplicableDrafts attribute so the default AutoDetectionDraft stays active): the draft detected from the document root's $schema URI must apply to property and nested subschemas, not only to the root level - sibling applicator crediting on the array side: tuple items, contains matches, and an explicit items: true must credit their indices to the unevaluatedItems accumulator - a composition branch carrying unevaluatedItems without the keyword on the array property itself currently renders code calling the missing collectUnevaluatedIndices() trait method - branch-level unevaluatedProperties: true and a nested-class branch's patternProperties matches must be credited to the outer accumulator - object applicators declared without an explicit type: object must not be silently dropped - unevaluatedProperties()->set() must reject a value that a successful branch's additionalProperties claims instead of committing it to the raw model data while leaving it invisible to the accessor Expected messages are pinned against wrapping formats captured from equivalent already-working paths (composition summary in direct-exception mode, nested-object wrapping); the nested class name is matched by regex where the generated class does not exist yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Array items (schema-form items, tuple-form items, and unevaluatedItems) now behave consistently with object properties: a transforming filter's output is persisted into the property's own storage, an already-transformed value is accepted directly, and mixed raw/transformed lists validate independently. TransformingFilterOutputTypePostProcessor and SerializationPostProcessor now recurse into ArrayItemValidator/ArrayTupleValidator/UnevaluatedItemsValidator the same way EnumPostProcessor already does, with a cycle guard for self-referencing schemas and a combined serializer so tuple and unevaluatedItems filters on the same property can't clobber each other's generated method name. Also fixes a validation-bypass bug surfaced by the above: _evaluatedItemIndices was never reset between separate validation passes, so a setTags() call could silently skip validating (and re-filtering) an index whose value had completely changed since a previous pass. Reset now happens before any validator runs for the property, ahead of composition validators that may credit the same slot earlier in the same pass. Migrates the remaining spl_object_hash usages in this package to spl_object_id, since all are internal identity-tracking uses (method-name suffixes, array-key deduplication) never serialized or exposed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix the review findings on unevaluatedProperties/unevaluatedItems: - AutoDetectionDraft caches the document dialect per file so a root $schema reaches every subschema; default dialect is now 2020-12 and a nested resource $schema no longer leaks to sibling subschemas. - Direct sibling array applicators credit their evaluated indices: a tuple items (and non-false additionalItems tail) deterministically, a sibling contains by recording matched indices, and items: true is treated as dead code that claims every index. - A composition branch carrying unevaluatedItems without the keyword on the array property now activates the evaluated-index field. - A branch-level unevaluatedProperties: true and a nested-class branch's patternProperties matches are credited to the outer accumulator. - Rename emitted catch variables to $exception (or drop the unused capture) and remove throwaway loop values in generated code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An untyped property resolved to type 'any', and PropertyFactory ran only the
'any' Draft entry. Every type-specific applicator -- properties,
patternProperties, additionalProperties, unevaluatedProperties, minLength,
minItems, ... -- was therefore silently dropped. JSON Schema applicators are
not gated on a type declaration, so they must apply whenever the instance is
of the relevant type.
Route the 'any' case to createUntypedProperty, which wires three layers onto a
single permissive property:
- Object applicators generate the nested class (type: object is forced only on
the copy handed to processSchema) and wire gated instantiation plus the
instanceof guard via ObjectModifier. The nested-class type it stamps is reset
to null afterwards and replaced by a `Nested | mixed` type hint, so the getter
stays permissive -- the slot may hold the nested object or any other value the
schema accepts. TypeCheckModifier(object) is deliberately not wired: it would
hard-reject non-object values.
- The universal 'any' modifiers run once, as before.
- Concrete-type validator factories run for the remaining types. Each factory
self-guards on keyword presence and each emitted validator self-gates on the
runtime type, so only present keywords contribute a validator and none of them
constrain the value's type. The non-factory type-shaping modifiers
(TypeCheck, IntToFloat, DefaultArrayToEmptyArray, Null) are skipped because
they are not keyword-gated and would reshape any-typed values.
A bare {} activates neither the object nor the scalar layer and behaves exactly
as before, so no empty nested classes are generated.
Extract the validator source-key tagging into a shared runModifier helper and
simplify createTypedProperty, which is no longer reachable with 'any'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The unevaluatedProperties()->set() and additionalProperties()->set() accessors could commit a value that a successful composition branch claims and rejects, leaving the model in a state its own constructor would refuse. Two independent defects caused this; either alone leaves the behaviour broken. _setUnevaluatedProperty never ran the composition validators at all — its comment wrongly asserted they cannot apply to a single dynamic key. A branch keyword that reaches keys the branch does not declare (additionalProperties, patternProperties, unevaluatedProperties, propertyNames, min/maxProperties) does decide whether the key is admissible. The setter now runs each _validateComposition_N against the candidate pair before the post-composition phase, with the same rollback discipline the additional-properties setter uses. The setter-side composition cache in ComposedItem.phptpl decided "nothing relevant changed" by intersecting the mutated keys against the branch's declared property names. A dynamic key never intersects that list, so the cache reported a hit and the branch was skipped — defeating even the additional-properties setter, which already invoked the validators. branchEvaluationDependsOnUndeclaredKeys() now gates the cache read: a branch whose outcome an undeclared key can flip is never served from the cache. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three defects in composition-aware setter/remove revalidation, all
pre-existing and surfaced while fixing the dynamic-key setter. Each was
reproduced red before the fix.
A plain setter for a declared property never revalidated a composition
branch whose outcome depends on keys the branch does not declare (a
branch minProperties/maxProperties/additionalProperties/...). The
composition post processor mapped a validator only to the properties
its branches declared, so a sibling property declared on the outer
schema but on no branch got no revalidation call and could commit a
value the branch rejects. A key-sensitive branch now maps its validator
to every declared property.
_removeUnevaluatedProperty ran only its local minProperties check and
performed no composition or post-composition revalidation, unlike the
additionalProperties remove path it should mirror. Removing a key could
flip a branch (dropping below a branch minProperties, orphaning a key a
branch used to claim) and leave a model its own constructor would
reject. The remove template now snapshots the mutated fields, revalidates
the post-removal state, and rolls back on rejection.
The composition template caches each branch's outcome in
_propertyValidationState for every mutable composition, but the field
was declared only when some branch declared a property. A composition
whose branches declare none (e.g. allOf: [{minProperties: 3}]) left the
field undeclared, so the template's write created it dynamically — a
deprecation as of PHP 8.4. The field is now declared for every mutable
composition regardless of branch shape.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # CLAUDE.md # composer.json # docs/source/combinedSchemas/not.rst # docs/source/generic/references.rst # src/Draft/AutoDetectionDraft.php # src/Model/Property/CompositionPropertyDecorator.php # src/Model/Validator/AbstractComposedPropertyValidator.php # src/Model/Validator/Factory/Arrays/ItemsValidatorFactory.php # src/PropertyProcessor/PropertyFactory.php # src/SchemaProcessor/PostProcessor/Templates/AdditionalProperties/RemoveAdditionalProperty.phptpl # src/SchemaProcessor/PostProcessor/Templates/AdditionalProperties/SetAdditionalProperty.phptpl # src/SchemaProcessor/PostProcessor/Templates/Populate.phptpl # src/Templates/Validator/ArrayContains.phptpl # src/Templates/Validator/ComposedItem.phptpl # src/Templates/Validator/ConditionalComposedItem.phptpl # src/Templates/Validator/SchemaDependency.phptpl # src/Utils/RenderHelper.php # tests/Basic/SchemaDependencyTest.php # tests/ComposedValue/ComposedAllOfTest.php # tests/Objects/ArrayPropertyTest.php
RemoveAdditionalProperty.phptpl, SetAdditionalProperty.phptpl, and
RemoveUnevaluatedProperty.phptpl each had a literal space before a
{% if %}...{% endif %} block emitting @throws. When the condition is
false the block renders nothing, leaving the docblock line as a bare
" *" with a trailing space. Moved the space inside the conditional so
it only appears alongside actual content.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Recovers testCompositionNestedInsideArrayBranchIsNotSilentlyDropped and its fixture, which had only ever been git-stashed before the master merge and never actually landed on the branch. Marked incomplete: a composition nested inside a composition branch on an array-typed property is silently dropped during generation, a pre-existing gap in the composition rendering path unrelated to unevaluatedItems itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the three remaining Phase 2 coverage-gap-test-plan items: - A bare boolean `true` composition branch under `unevaluatedProperties` contributes nothing to the evaluated set and does not crash on the branch's absent nested schema. - `unevaluatedItems` declared on a non-array-typed property is silently ignored (no array-index validator is emitted), while a sibling `unevaluatedProperties: false` still activates normally. - `if`/`then`/`else` on an array property under `unevaluatedItems` credits whichever of `then`/`else` actually ran, exercising ConditionalComposedItem.phptpl's array-side branch for the first time. No defect found — this closes a coverage gap in already- correct code, not a bug fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ng patternProperties
AdditionalProperties.phptpl's pattern-exclusion loop referenced an
undefined $property instead of the enclosing foreach's $propertyKey.
Every key, matching the pattern or not, hit preg_match($pattern, null),
which throws a TypeError under PHP 8's strict internal-function typing
-- the surrounding catch (\Exception $exception) does not catch
TypeError, so construction crashed uncaught for any input on any
schema combining additionalProperties: {schema} with a sibling
patternProperties, not just producing a wrong validation result.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the remaining Phase 3 coverage-gap-test-plan items: - Proves the composition cache-hit skip for a setter whose property isn't wired to any _validateComposition_N call, via reflection-based state corruption (a value that would fail revalidation, left untouched to prove the setter never re-validates). - Proves _setUnevaluatedProperty's same-value early return actually skips validation/rollback bookkeeping, not just that revalidating the same valid value happens to succeed, via an error-registry sentinel-identity check. - Direct-exception-mode counterpart to the existing collect-errors test for a remove() that both drops below minProperties and flips a composition branch: only the first applicable exception surfaces, and rollback restores state. - harvestCompositionPropertyNames() recursion into patternProperties and a composition nested inside a branch. - The untyped (bare production-library class) unevaluatedProperties accessor and denyAdditionalProperties() suppressing the accessor entirely. Two items closed with no test needed: 3.2 was already covered by an existing test using the harness's direct-exception-mode default. 3.1's existing test method was renamed and rewritten in place since its docblock and assertions did not actually prove a cache skip. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds regression coverage for harvestCompositionPropertyNames()'s if/then/else recursion, found by cross-referencing a fresh coverage report against the earlier gap analysis: allOf/anyOf/oneOf and if/then/else are two separate recursive code paths in the harvester, and the existing nested-composition test only exercised oneOf. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cross-referenced a fresh --coverage-clover run against the original
gap analysis (57 uncovered lines) rather than trusting behavioral
verification alone. Closes two of the four remaining lines:
- UnevaluatedItemsValidatorFactory's dead-code classifier falls
through to "not dead code" for items: null (not a valid items
value) rather than misclassifying it as one of the three recognised
dead-code shapes.
Also documents a genuine, previously-undiscovered bug found while
tracing the other two lines: a composition branch's own
unevaluatedProperties never sees properties the enclosing schema (or
a sibling branch) declares -- only the reverse direction (branch
claims propagating up to an outer unevaluatedProperties) was ever
built. `{properties: {name}, allOf: [{unevaluatedProperties: false}]}`
wrongly rejects {name: "Alice"} even though name is declared and
validated by the enclosing schema. Root cause and revisit trigger are
in implementation-plan.md item 20; surfaced here via markTestIncomplete
since the fix touches how a branch's nested class computes its
evaluated set, not a single call site.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tion Post-implementation-review items 13 and 17 both described "composition nested inside a composition branch on an array-typed property" but turned out to be two independent bugs at different layers, not one: - Item 17 (activation-detection gap): UnevaluatedPropertiesPostProcessor never recursed into a branch's own nested composition when the branch is array-typed (no nested Schema to recurse through via getNestedSchema(), unlike object-typed branches). Result: a fatal "Error: Call to undefined method ...::collectUnevaluatedIndices()" -- the innermost branch's unevaluatedItems validator rendered and called into CompositionEvaluationTrait, but the trait was never added to the class. Fixed by recursing through the branch's wrapped property's own validators, mirroring how activateValidatorsInBranch() already walks this exact structure for the activation step itself. - Item 13 (annotation-aggregation gap): once activation is correctly triggered, a branch that is itself purely a nested composition (no direct items/contains of its own) never had its nested composition's correctly-tracked claimed indices read back into its own tracked slot, so the outer unevaluatedItems: false over-rejected every index instead of just the ones actually left over. Fixed by CompositionPropertyDecorator::getNestedCompositionSlotKeys(), unioned into the branch's evaluated-index set in ComposedItem.phptpl. Both fixtures were re-tested against each fix in isolation to confirm the two bugs are independent, not the same defect surfacing twice. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
warnIfVacuousBranch() never reached a `not: {}` branch because
NotValidatorFactory never forwarded which branch index inherited its type
from the parent - inheritPropertyType() now returns that index set (tuple
return replaces the easy-to-forget by-ref out-param) instead of silently
dropping it for one composition keyword.
Also fixes TypeCheck::buildNegatedJsonSchemaTypeCheck() wrongly rejecting an
empty JSON object `{}` for a multi-type object candidate whenever its sibling
type wasn't `array` (json_decode can't tell `{}` from `[]`, and the existing
'array'-side carve-out had no 'object'-side mirror).
Both found via code review; each ships with a red-then-green regression test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Profiled UnevaluatedPropertiesPostProcessor::needsActivation() against a synthetic worst-case schema (many top-level classes sharing one deep $ref chain, no unevaluatedProperties anywhere to short-circuit the walk). Cost tracks (containers x depth) as predicted but stays in the 2-7% band of total generation time even under this adversarial shape - not a hot path, and a persistent Schema-level cache would carry real invalidation risk against post processors that still mutate schemas after the walk runs. Closed without adding caching; see the plan for the full measurement. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… audit Walked every composition-branch occurrence of the nine undeclared-key-sensitive keywords across the test fixture set (43 real hits) and traced through how additionalProperties/patternProperties/propertyNames/unevaluatedProperties are scoped per-branch, not per-schema: a branch's own declared names are the only ones excluded from its own undeclared-key check, so any other schema property (declared by a sibling branch or the base schema) is genuinely relevant, not merely a conservative safety margin. No real fixture shows the "map every property" fallback over-mapping in practice. Closed without changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Measured the two isolatable components of overhead since a clean before/after checkout isn't available on this branch (155 commits diverged from master, spanning unrelated multi-draft work): the new unevaluatedProperties tests are 3.5% of total suite wall-clock, and the always-on UnevaluatedPropertiesPostProcessor adds a 5.1% marginal generation-time tax to schemas that never opt into the feature (measured via reflection A/B over 137 real existing fixtures) - combined estimate lands in the 6-9% range, under the item's own >10% investigate-further threshold. Closed without adding activation-walk caching, consistent with item 1. Also adds a tracked, deliberately-failing regression test for a genuine but unrelated bug found while assembling the benchmark batch: a schema whose derived class name collides with a PHP-reserved word (e.g. "ReadOnly", "readonly" reserved since PHP 8.1) generates uncompilable PHP instead of failing cleanly at generation time. Full analysis and a related second collision case (with the generated class's own fixed imports) in .claude/topics/reserved-php-classname-collision/analysis.md - deferred as a separate topic, out of scope for the unevaluated-properties feature. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CompositionTypeHintDecorator::getTypeHint() recursed indefinitely on a schema
like {type: array, allOf: [{$ref: "#"}], unevaluatedItems: false} - the
composed branch's wrapped property carries the same decorator instance again.
Fixed with a per-instance recursion-depth guard mirroring
ArrayTypeHintDecorator's existing pattern for the analogous cycle; on
re-entry it skips itself for the nested getTypeHint() call instead of
reapplying, without degrading type hints for genuinely nested (non-cyclic)
composition.
Fixing that let generation get further and surface a second, previously
undocumented recursion in the same subsystem:
UnevaluatedPropertiesPostProcessor::propertyHasBranchUnevaluatedItems()
recurses into a branch's wrapped property with no cycle guard whenever the
branch has no nested Schema - true for every array-typed branch, so true for
this same self-referencing shape. Fixed with a $seen map keyed on
file+pointer (not object identity, since getOrderedValidators() returns
fresh clones per call), mirroring needsActivation()'s existing schema-level
guard.
Both verified red-then-green by reverting each fix in isolation and
confirming Xdebug's own infinite-loop guard catches the un-fixed method.
A third, deeper bug surfaced once generation started succeeding:
constructing an instance still stack-overflows, because allOf: [{$ref: "#"}]}
applied directly to a property (not through items) is a degenerate
constraint with no base case. Verified this doesn't affect the actually
useful items: {$ref: "#"} recursive pattern, which works correctly. Deferred
as its own topic (.claude/topics/self-composition-runtime-recursion/) since
a real fix is a design decision (reject at generation time vs. runtime cycle
memoization), not a mechanical one.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ty audit Read all 7 flagged call sites in PropertyFactory.php, RefResolver.php, PropertyAttributeSynthesizer.php, and SchemaProcessor.php in full context - every one is already correctly guarded (an early-return guard clause, a directly-preceding null check, a ternary, or safe by construction). No fix needed. The stress fixture built to raise confidence beyond static reading (an anyOf-based mutual-reference pair, exercising the two least-obviously-covered SchemaProcessor.php sites) surfaced a much larger, separate bug instead: constructing an instance of a mutually-referencing anyOf/oneOf composition of objects stack-overflows, since each branch must stay independently instantiable (unlike allOf, which flattens mutually-referencing branches at generation time - verified by inspecting the existing passing allOf fixture's generated output). Folded into the runtime-recursion topic item 15 already opened, since it changes that topic's fix-option recommendation. Surfacing test added (generation succeeds; construction is known to hang, so the test stops short of it and is marked incomplete). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Investigated whether the down-propagation gap (a composition branch's own unevaluatedProperties/unevaluatedItems can't see names/indices the enclosing schema or a sibling branch already claims) is tractable to fix now. It is not, as a single change: the enclosing schema's own declared names are static, generation-time-known information that could in principle be baked into a branch's generated validator call, but a sibling branch's claims can be instance-dependent and branches validate one at a time in a single forward pass (confirmed in ComposedItem.phptpl), so a spec-correct fix needs a two-pass evaluation model, not a call-site patch. Shipping only the tractable half was considered and rejected - it would leave the sibling-branch direction silently broken behind a passing test that never exercises it. Confirmed the array side has the identical defect (the item's own "revisit trigger" had left this unverified): a sibling tuple items claim on an array property is invisible to an allOf branch's own unevaluatedItems: false. Added a markTestIncomplete regression test mirroring the existing object-side one, so the gap has a tracked, failing surfacing mechanism on both sides. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…agation A composition branch's own unevaluatedProperties/unevaluatedItems has no visibility into property names or indices declared by the enclosing schema or a sibling branch - only the reverse direction (branch to enclosing) is implemented. A real fix would need a two-pass evaluation model, which is provably ambiguous for anyOf/oneOf (two sibling branches can each gate on the other's claims with no unique consistent outcome). Rather than ship a partial fix or silently produce incorrect validation, generation now throws a new UnsupportedSchemaFeatureException (a SchemaException subclass) when this unsupported shape is detected, across allOf/anyOf/oneOf/if/then/else (not is exempt by design). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the JSON Schema 2019-09 keywords
unevaluatedPropertiesandunevaluatedItemswith full integration into the library's feature set: composition (allOf/anyOf/oneOf/if-then-else/not),$refresolution, mutable models (setters,populate(), accessor mutators), error collection and direct-exception mode, serialization, transforming filters, and the accessor post-processor family.Both keywords consume the annotation results of sibling applicators: a key/index counts as evaluated only when a sibling applicator — including a successful composition branch — actually claimed it at runtime. A static approximation is impossible under
anyOf/oneOf/conditionals, so the generated code tracks branch outcomes at validation time.Object side —
unevaluatedPropertiesUnevaluatedPropertiesValidator(schema form) /NoUnevaluatedPropertiesValidator(falseform) withUnevaluatedPropertiesValidatorFactory, registered on theobjecttype for Draft 2019-09.Schema::addPostCompositionValidator()→_executePostCompositionValidators()on the generated model), guaranteeing spec ordering:properties→propertyNames→patternProperties→additionalProperties→ compositions →unevaluatedProperties.patternPropertiesmatches with passing values, and the per-branch composition cache (see below). Keys the schema-form validator itself accepts are recorded in_evaluatedPropertyKeysso an enclosing schema'sunevaluatedPropertiescredits them, and nested branch classes expose their evaluated set via an internal_getEvaluatedProperties()method.Array side —
unevaluatedItemsUnevaluatedItemsValidator/NoUnevaluatedItemsValidatorwith factory, registered on thearraytype at priority 101 so the validator runs after composition validators (priority 100) in the property's validator chain; non-array values of multi-typed properties short-circuit via anis_array()guard._compositionAnnotatedslot per composition validator (slot keys<property>_<n>); branch claims are derived from the branch'sitems(schema/tuple form),additionalItems, and runtimecontainsmatches.containssupport: the anonymous contains validator was promoted to a namedArrayContainsValidatorwith an opt-in per-index match map, so a composition branch can credit exactly the indices itscontainsmatched (including combineditems+containsbranches andminContains: 0).unevaluatedItemsinside a successful branch credits its indices to the outer accumulator through the shared_evaluatedItemIndicesfield, with snapshot/rollback so a failing branch leaks nothing.Runtime evaluation tracking (composition integration)
ComposedItem.phptplandConditionalComposedItem.phptplwrite per-branch results into the_compositionEvaluationscache when tracking is activated: branch claims from declared properties / pattern matches / non-falseadditionalProperties, or a reference to the nested branch instance. Failed branches contribute nothing; whole-composition failure restores the pre-run cache;notbranches are unconditionally rolled back (negative applicators never produce annotations).ConditionalPropertyValidatornow extendsAbstractComposedPropertyValidator, bringingif/then/elseunder the same tracking machinery (slots forif,then,else; a succeedingifcontributes its own claims alongsidethen).UnevaluatedPropertiesPostProcessorwalks the schema graph (composition branches, nested schemas,$reftargets — cycle-safe) and only instruments schemas that can actually reach one of the keywords. Schemas without the keywords generate unchanged output.CompositionEvaluationTrait(collectEvaluatedProperties(),collectUnevaluatedKeys(),collectUnevaluatedIndices()).Mutability and cross-state validation
oneOfdiscriminator flip) is rejected.populate()validates the merged candidate state the same way; rollback now uses a Schema-level registry (addRollbackProperty()/addAccessorCacheProperty()) instead of hard-coded field lists inPopulate.phptpl._setAdditionalProperty/_removeAdditionalPropertygained full cross-state discipline (composition re-runs against the candidate state, snapshot/rollback of caches and collections on rejection) so dynamic-key mutations interact correctly with unevaluated tracking.Opt-in accessor —
UnevaluatedPropertiesAccessorPostProcessorFollows the accessor-pattern from the additional/pattern-properties post processors:
{Model}UnevaluatedPropertiescompanion class with narrowed signatures; untyped schemas use the production-libraryUnevaluatedPropertiesAccessor/ImmutableUnevaluatedPropertiesAccessor._setUnevaluatedPropertyshim guards against keys that belong elsewhere — declared properties,patternPropertiesmatches, and names/patterns declared inside composition branches (harvested recursively, reported with RFC 6901 pointers viaRegularPropertyAsUnevaluatedPropertyException)._unevaluatedPropertiescollection) is only generated when unevaluated keys are actually reachable; a siblingadditionalPropertiesoftrue/false/{schema}suppresses it entirely.Generation-time diagnostics
additionalPropertiesshape incl. thedenyAdditionalProperties()flag; array side foritems: false, schema-formitems, and tupleitems+additionalItems: false.SchemaExceptionfor invalid keyword values (non-boolean, non-object).Utils\JsonSchema(shared by the accessor post processors), andRenderHelpergained PCRE-ready pattern export helpers.Serialization and filters
_unevaluatedPropertiesis flattened intotoArray()/toJson()output; aUnevaluatedPropertiesSerializer.phptplhandles transforming filters (e.g.DateTimevalues), mirroring the additional-properties serializer.TransformingFilterOutputTypePostProcessorprocesses the unevaluated validation property so filtered values keep working through the accessorset()path.Infrastructure changes
RenderQueuenow runs all post processors before rendering any class, so a post processor may attach methods to nested branch schemas regardless of queue order.ExtractedMethodValidatormethod names are made instance-unique to fix a collision when two identical composition schemas exist on one class.php-micro-templaterequirement bumped to^1.11.0.Production library (branch
jsonSchemaDraft2019of wol-soft/php-json-schema-model-generator-production)New classes this branch depends on:
Accessor/UnevaluatedPropertiesAccessor,Accessor/ImmutableUnevaluatedPropertiesAccessor,Traits/CompositionEvaluationTrait, and the exceptionsUnevaluatedPropertiesException,InvalidUnevaluatedPropertiesException,UnevaluatedItemsException,InvalidUnevaluatedItemsException,RegularPropertyAsUnevaluatedPropertyException(all pointer-aware). A tagged release + version constraint must replace the branch pin before this reachesmaster.Documentation
object.rst/array.rst: dedicated sections for both keywords (forms, exception surfaces, dead-code behaviour).allOf,anyOf,oneOf,if,not): a "Property and item evaluation propagation" section, including the spec-mandated omitted-vs-explicitadditionalPropertiesdistinction.references.rstnote on$refcontribution, a newunevaluatedPropertiesAccessorPostProcessorpage, fixes to the additional-properties accessor page, and several pre-existing Sphinx warnings resolved.Tests
Extensive coverage under the multi-draft expansion infrastructure (Draft 2019-09 + 2020-12): validator behaviour in both error modes, composition propagation (incl.
$ref-resolved branches and recursive self-references), object- and array-side mutability suites, accessor round-trips with filters and serialization, dead-code warnings, and generation-time error cases.Postponed / known limitations
prefixItems(Draft 2020-12) anddependentSchemasannotation contribution — deferred until those keywords land; a skipped test pins the intendeddependentSchemasbehaviour.$refas a composition branch and self-referencing array compositions are blocked by pre-existing limitations (composition nested-schema expectation; type-hint recursion) — both tracked via incomplete tests.unevaluatedItemsby design: items remain accessible through the typed property; the validator is a pure assertion.🤖 Generated with Claude Code