Skip to content

A multi-type property including object with a composition rejects every object value #191

Description

@wol-soft

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions