Composition-implied objects
A property (or composition branch) can be object-shaped without ever declaring
"type": "object" itself — either because a composition guarantees it, or because it carries
keywords that only make sense for objects. The generator detects both cases, but treats them
differently. A composition that guarantees object-ness (object-asserting) is treated exactly
like an explicit "type": "object": a non-object value is rejected. A schema that only carries
object-constraining keywords (object-describing) is merely recognized as object-shaped: a
non-object value still passes through unchanged, and at a site that defines its own generated
class it is rejected unless setImplicitObjectComposition(true) is set.
Object-asserting: composition guarantees object-ness
An allOf whose branches, taken together, guarantee that every valid value is an object is
object-asserting even if no individual branch (and not the property itself) declares
"type": "object" directly — for example an allOf of $refs that each resolve
(possibly through further nested allOf/$ref chains) to an object schema:
{
"$id": "example",
"type": "object",
"properties": {
"person": {
"allOf": [
{ "$ref": "#/definitions/identification" },
{ "$ref": "#/definitions/employment" }
]
}
},
"definitions": {
"identification": {
"allOf": [
{
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
}
]
},
"employment": {
"type": "object",
"properties": { "age": { "type": "integer" } }
}
}
}
person is routed through the same object path as an explicit "type": "object" property: a
real class is generated for it, instantiated for object values, and validated with an
unconditional instanceof check. Neither person nor identification declares its own
type; the generator establishes object-ness by resolving $ref chains and allOf
branches statically at generation time. Both name (from identification) and age (from
employment) end up as accessors on the same generated class for person:
public function getPerson(): ?PersonClass;
# PersonClass
public function getName(): string;
public function getAge(): ?int;
This re-routing applies wherever a property is processed — root level, nested properties, array
items, schema dependencies values, and base-level $ref schema files alike — but only for
allOf as the outer keyword. anyOf/oneOf/if/not keep each branch’s own runtime
value identity and are fixed at the branch level instead: a branch that is itself an
object-asserting allOf (or $ref to one) is routed through the object path when it is
built, but the outer anyOf/oneOf/if/not composition itself is never collapsed into
a single class the way an outer allOf is.
Note
Mixing an object-asserting branch with a scalar-typed branch in the same allOf is a
generation-time conflict, exactly as it is for an explicit "type": "object" branch — see
the conflicting-types note in All Of.
Object-describing: bare validation keywords, no type
A property or branch that carries only object-constraining keywords (properties,
required, patternProperties, additionalProperties, propertyNames,
minProperties, maxProperties, dependencies) and no type at all is
object-describing. Per strict JSON Schema semantics, these keywords constrain object values
but are vacuously satisfied by any non-object value:
{
"$id": "example",
"type": "object",
"properties": {
"person": {
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
}
}
}
public function setPerson(mixed $person): static;
public function getPerson(): mixed;
An object value is instantiated and validated against the declared constraints; a non-object
value passes through completely unchanged — so the getter/setter type hint stays mixed
instead of the representation class, since a non-object value would otherwise violate a typed
return. Passing ["name" => "Hannes"] returns an instantiated representation object; passing
42 returns 42 unchanged; passing [] (an object missing the required name) is
rejected:
Invalid nested object for property 'person':
- Missing required value for 'name'
The generator emits a generation-time warning for an object-describing property, array item or
composition branch — since it is easy to write one by accident (forgetting "type": "object")
and get silent pass-through instead of the intended validation:
Property 'person' carries object-constraining keywords (eg. 'properties', 'required')
without a 'type' declaration and does not constrain non-object values
Note
A schema dependencies value is the one site that does not take part in this: an
untyped dependency schema has "type": "object" forced onto it before any classification
happens, so it is treated as object-asserting — a non-object value is rejected rather than
passed through — and no warning is emitted. Declare "type": "object" explicitly there to
make what you get match what you wrote.
Note
An object-describing branch does not conflict with a scalar-typed sibling in the same
allOf — unlike the object-asserting case above, it is vacuously satisfied by non-object
values, so e.g. allOf: [{properties: {...}, required: [...]}, {type: "string"}] is
satisfiable (by any string that also happens to satisfy the properties/required
constraints when it is an object — which a string never is, so in practice only the scalar
branch’s constraint is ever checked).
anyOf / oneOf with object-describing branches
Because an object-describing branch is vacuously satisfied by any non-object value, a non-object
value satisfies every bare-validator branch in a composition at once. This matters for
oneOf, which requires exactly one branch to match:
{
"$id": "example",
"type": "object",
"properties": {
"person": {
"oneOf": [
{ "properties": { "name": { "type": "string" } }, "required": ["name"] },
{ "properties": { "companyName": { "type": "string" } }, "required": ["companyName"] }
]
}
}
}
A non-object value such as 42 vacuously satisfies both bare branches at once, which violates
oneOf’s “exactly one” requirement, so it is rejected:
Invalid value for 'person' declined by composition constraint
Requires to match one composition element but matched 2 elements
- Composition element #1: Valid
- Composition element #2: Valid
For anyOf (which only requires at least one match), the same non-object value is accepted,
since matching every branch still satisfies “at least one.”
Array items and other schema-creation contexts
Composition-implied object detection applies everywhere a subschema is turned into a property or
class, not just to named object properties — array items, schema dependencies values, and
base-level $ref schema files all resolve composition-implied object-ness the same way (the
object-describing case is the exception noted above: dependencies force-asserts it):
{
"$id": "example",
"type": "object",
"properties": {
"members": {
"type": "array",
"items": { "$ref": "#/definitions/person" }
}
},
"definitions": {
"person": {
"allOf": [
{
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
}
]
}
}
}
Each array item is instantiated and validated exactly like an item referencing an explicit
"type": "object" definition would be, even though person never declares its own type.
Class-defining compositions must resolve to a definite object
A composition that defines its own generated class — a schema file’s root, or a $ref target
that is parsed as a top-level schema in its own right — must resolve to a definite object:
every value the composition accepts has to be representable by the single generated class. Unlike
the property-level detection above, which lets an object-describing property pass non-object
values through unchanged, a class-defining composition has no such escape hatch: by default it
rejects an object-describing composition outright:
{
"$id": "example",
"oneOf": [
{ "properties": { "name": { "type": "string" } }, "required": ["name"] },
{ "properties": { "companyName": { "type": "string" } }, "required": ["companyName"] }
]
}
Neither branch declares "type": "object", so the composition as a whole is
object-describing, not object-asserting — per strict JSON Schema semantics a non-object value
(e.g. a bare string) is a valid instance of this schema too, but there is no non-object
representation this generated class could produce for it. Generation is rejected:
Composition for 'Example' in file '...' does not resolve to a definite object and cannot be
represented as a generated class: add an explicit '"type": "object"' constraint, or enable
'GeneratorConfiguration::setImplicitObjectComposition(true)' to accept it
A composition that resolves to no object at all — one containing a scalar-typed branch, a vacuous
branch such as true or {}, no branches at all, or a root type listing more than just
"object" — is rejected too, but with the second half of the message replaced:
Composition for 'Example' in file '...' does not resolve to a definite object and cannot be
represented as a generated class: if every value it accepts is meant to be an object, declare
'"type": "object"' on the schema itself
The opt-in flag is deliberately not offered there: it widens acceptance from object-asserting to object-describing and no further, so it cannot rescue a composition that genuinely accepts non-object values. Declaring the type does resolve it, but unlike the object-describing case it changes which values the schema accepts rather than stating what it already means — which is why the message makes it conditional rather than an instruction.
Declaring "type": "object" on the schema itself always resolves it to a definite object
regardless of what its branches declare — the explicit type is the assertion, and the branches
only narrow further.
{
"$id": "example",
"type": "object",
"oneOf": [
{ "properties": { "name": { "type": "string" } }, "required": ["name"] },
{ "properties": { "companyName": { "type": "string" } }, "required": ["companyName"] }
]
}
An if/then/else composition resolves to a definite object only when both the then
and the else branch do — every value takes exactly one of the two paths, so the whole
composition is only guaranteed object-ness when both paths are. A missing then or else
leaves that path fully unconstrained and is treated the same as a branch that doesn’t assert
object-ness.
Note
This requirement applies only to a schema file’s root and to a $ref target parsed as its
own top-level schema. A named property, array item, or schema dependencies value never
triggers this rejection — an object-describing schema there follows the property-level
behaviour described in Object-describing: bare validation keywords, no type above: a
non-object value passes through unchanged instead of being rejected.
By default an object-describing composition (as opposed to object-asserting, or a composition that
doesn’t resolve to an object at all) is rejected. Enable
setImplicitObjectComposition(true) (see Configuring the generator) to treat an object-describing
class-defining composition exactly as if it had declared "type": "object" itself, instead of
rejecting it.
Note
Both the requirement and the flag apply to compositions. A schema file whose root carries
only object keywords — a bare properties/required schema with no allOf/anyOf/
oneOf/if, no $ref and no type — is not a composition, and is skipped before
this check runs: it produces no generated class and no message, whether or not the flag is
enabled. Add "type": "object" to such a root to have a class generated for it.