Summary
A property whose declared type is a multi-type list containing object, combined with a
composition (anyOf / allOf / oneOf / if) whose branch resolves to an object, rejects every
object value at runtime — with an error comparing two generated class names:
Invalid class for 'p': requires 'Root_Root_P5d2593f4976038d6db2cbff598e75752', got 'Root_P'
The other declared types still work; only object values are affected. This includes
["object", "null"], the ordinary way to spell a nullable object, which makes the shape easy to hit
by accident.
Pre-existing on master. Found while working on #181.
Reproduction
{
"type": "object",
"properties": {
"p": {
"type": ["object", "null"],
"anyOf": [
{
"type": "object",
"required": ["a"]
}
]
}
}
}
new Root(['p' => ['a' => 1]]);
Invalid value for 'p' declined by composition constraint
Requires to match at least one composition element
- Composition element #1: Failed
* Invalid class for 'p': requires 'Root_Root_P5d2593f…', got 'Root_P'
{"a": 1} satisfies the branch: it is an object and it has a. It must be accepted.
Boundary
Only the parent property's declared type matters. The branch below is the same object-typed branch
in every row:
parent type |
object value |
"object" |
accepted |
["object"] |
rejected |
["object", "string"] |
rejected |
["object", "null"] |
rejected |
| (absent) |
accepted |
The branch's own spelling makes no difference — a branch declaring "type": "object" and a branch
declaring "type": ["object", "string"] both fail. Applies to anyOf, allOf and if/then/else
alike.
Note the ["object"] row is a separate defect (#189) rather than part of this one, and is already
fixed on the #181 branch; the two remaining multi-type rows are what this issue is about.
Cause
PropertyFactory::createMultiTypeProperty() builds one sub-property per listed type and routes the
object entry through createObjectProperty(), which generates a nested class and attaches an
instantiation decorator. The value handed to the property is therefore instantiated into the
property's own generated class before the composition validator runs.
The composition branch is processed independently and generates a second class of its own, with an
InstanceOfValidator against it. That validator then sees an instance of the property's class and
fails — the two classes describe the same data but are unrelated types.
With a single "object" type, or with no type at all, only one class is involved and nothing
compares them.
Suggested direction
The branch check needs to compare against the value as the branch understands it, rather than
against whichever class instantiated it first. Options worth evaluating:
- Have the multi-type object sub-property and the composition branch share one generated class where
the branch's constraints permit it.
- Reset the branch's input to the raw model data (as
ComposedItem.phptpl already does between
branches) so the branch validates the array rather than an instance.
- Skip the branch's
InstanceOfValidator when the enclosing property is multi-type, and rely on the
branch's own structural constraints instead.
Test coverage to add
No existing fixture combines a multi-type property with a composition, which is why this has gone
unnoticed. Worth covering:
["object", "null"] and ["object", "string"] parents, each with anyOf, allOf and
if/then/else, asserting that a satisfying object is accepted;
- a non-satisfying object still rejected, with the branch's real reason rather than a class
mismatch;
- the non-object member of the union still accepted;
- the single-type
"object" parent as the control that must keep working.
Related
Fixed on the #181 branch, in the same area but a different mechanism: the same shape with an
untyped branch ({"required": ["a"]} with no type) also rejected every object value, because
the parent's type was injected into the branch and turned it into a multi-type branch with its own
class. That injection is gone, so untyped branches now work; an explicitly object-typed branch still
does not, which is this issue.
Summary
A property whose declared
typeis a multi-type list containingobject, combined with acomposition (
anyOf/allOf/oneOf/if) whose branch resolves to an object, rejects everyobject value at runtime — with an error comparing two generated class names:
The other declared types still work; only object values are affected. This includes
["object", "null"], the ordinary way to spell a nullable object, which makes the shape easy to hitby accident.
Pre-existing on
master. Found while working on #181.Reproduction
{ "type": "object", "properties": { "p": { "type": ["object", "null"], "anyOf": [ { "type": "object", "required": ["a"] } ] } } }{"a": 1}satisfies the branch: it is an object and it hasa. It must be accepted.Boundary
Only the parent property's declared type matters. The branch below is the same object-typed branch
in every row:
type"object"["object"]["object", "string"]["object", "null"]The branch's own spelling makes no difference — a branch declaring
"type": "object"and a branchdeclaring
"type": ["object", "string"]both fail. Applies toanyOf,allOfandif/then/elsealike.
Note the
["object"]row is a separate defect (#189) rather than part of this one, and is alreadyfixed on the #181 branch; the two remaining multi-type rows are what this issue is about.
Cause
PropertyFactory::createMultiTypeProperty()builds one sub-property per listed type and routes theobjectentry throughcreateObjectProperty(), which generates a nested class and attaches aninstantiation decorator. The value handed to the property is therefore instantiated into the
property's own generated class before the composition validator runs.
The composition branch is processed independently and generates a second class of its own, with an
InstanceOfValidatoragainst it. That validator then sees an instance of the property's class andfails — the two classes describe the same data but are unrelated types.
With a single
"object"type, or with no type at all, only one class is involved and nothingcompares them.
Suggested direction
The branch check needs to compare against the value as the branch understands it, rather than
against whichever class instantiated it first. Options worth evaluating:
the branch's constraints permit it.
ComposedItem.phptplalready does betweenbranches) so the branch validates the array rather than an instance.
InstanceOfValidatorwhen the enclosing property is multi-type, and rely on thebranch's own structural constraints instead.
Test coverage to add
No existing fixture combines a multi-type property with a composition, which is why this has gone
unnoticed. Worth covering:
["object", "null"]and["object", "string"]parents, each withanyOf,allOfandif/then/else, asserting that a satisfying object is accepted;mismatch;
"object"parent as the control that must keep working.Related
Fixed on the #181 branch, in the same area but a different mechanism: the same shape with an
untyped branch (
{"required": ["a"]}with notype) also rejected every object value, becausethe parent's type was injected into the branch and turned it into a multi-type branch with its own
class. That injection is gone, so untyped branches now work; an explicitly object-typed branch still
does not, which is this issue.