Skip to content

fix: apply required and additionalProperties to an empty object - #8

Merged
Coysh merged 1 commit into
mainfrom
fix/validate-required-fields-on-an-empty-object
Aug 6, 2026
Merged

Coysh merged 1 commit into
mainfrom
fix/validate-required-fields-on-an-empty-object

Conversation

@Coysh

@Coysh Coysh commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

SchemaValidator accepted {} against every published schema, however many fields that schema declared required. Confirmed against inventory.v1, system.v2, updates.v2, logins.v1 and backup.v3 — all five returned no errors at all, while a single unrecognised field was correctly rejected by all five.

Why

json_decode('{}', true) returns [], and array_is_list([]) is true, so check() dispatched an empty object to checkArray() — which returns immediately when the schema declares no items. checkObject() holds both required and additionalProperties, and was never reached. matchesType() already carried this exact exception one screen down ('object' accepts $value === []), so the type gate passed first.

Downstream the effect was not a lenient accept but a worse error: a payload the validator was supposed to refuse by name instead reached code that indexed a key which was not there. An inventory report of {} returned a 500 with a correlation id rather than a 422 naming the missing field. A manifest whose encryption block had been emptied passed both the declaration schema and the manifest signature check before dying on an undefined key.

What changed

Dispatch consults the schema before PHP's shape. A node the schema declares as type: object reaches checkObject() even when it arrives empty. A node with no declared type keeps the old behaviour, and an empty array against type: array is still a list — so nothing that validated before stops validating.

tests/SchemaValidatorTest.php is new and covers the validator's own behaviour rather than any one schema's rules: every published schema refuses {}, a nested empty object is refused, an empty list is still a list, and every valid fixture still validates unchanged.

It survived 368 tests because the suite empties an object by unsetting one key at a time, so every test payload kept at least one key and the fully-empty case was never constructed.

Not done

No schema file was edited and no fixture was regenerated — the rules were already published, they were simply not being applied. Nothing was relaxed for existing callers: a connector sending {} was already broken and now learns so with a 422 that names the fields.

Checks

  • composer test — 384 passed (2,797 assertions); was 368 before the new file
  • composer phpstan — no errors
  • Version moved to 1.7.0 in both composer.json and Protocol::VERSION, as VersionTest requires

This is first in the release order, so nothing downstream can consume the fix until it is tagged and Packagist serves it.

SchemaValidator accepted `{}` against every published schema, however many
fields that schema declared required. Confirmed against inventory.v1,
system.v2, updates.v2, logins.v1 and backup.v3: all five returned no errors.

json_decode('{}', true) returns [], and array_is_list([]) is true, so check()
dispatched an empty object to checkArray() - which returns immediately when the
schema declares no `items`. checkObject() holds both `required` and
`additionalProperties`, and was never reached. matchesType() already carried
this exact exception one screen down, where 'object' accepts $value === [], so
the type gate passed first and the payload sailed through everything after it.

Dispatch now consults the schema before PHP's shape: a node the schema declares
as type `object` reaches checkObject() even when it arrives empty. A node with
no declared type keeps the old behaviour, and an empty array against type
`array` is still a list - so nothing that validated before stops validating,
which the new test asserts directly.

The effect downstream was not a lenient accept but a worse error. A payload the
validator was supposed to refuse by name instead reached code that indexed a
key which was not there: an inventory report of `{}` returned a 500 with a
correlation id rather than a 422 naming the missing field, and a manifest whose
encryption block had been emptied passed both the declaration schema and the
manifest signature check before dying on an undefined key.

It survived 368 tests because the suite empties an object by unsetting one key
at a time, so every test payload kept at least one key and the fully-empty case
was never constructed. tests/SchemaValidatorTest.php now covers the validator's
own behaviour rather than any one schema's rules.

No schema file was edited and no fixture was regenerated. The rules were
already published; they were not being applied.
@Coysh
Coysh merged commit 85aa58c into main Aug 6, 2026
3 checks passed
@Coysh
Coysh deleted the fix/validate-required-fields-on-an-empty-object branch August 17, 2026 17:04
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.

1 participant