Skip to content

feat: support unevaluatedProperties in Draft 2019-09 - #931

Open
tomatotomata wants to merge 4 commits into
jsonrainbow:mainfrom
tomatotomata:codex/unevaluated-properties
Open

feat: support unevaluatedProperties in Draft 2019-09#931
tomatotomata wants to merge 4 commits into
jsonrainbow:mainfrom
tomatotomata:codex/unevaluated-properties

Conversation

@tomatotomata

@tomatotomata tomatotomata commented Aug 6, 2026

Copy link
Copy Markdown

Summary

This adds a focused proof of concept for unevaluatedProperties in Draft 2019-09, following the scope discussed in #907.

The implementation derives evaluated property names from properties, patternProperties, and direct allOf branches. That covers the motivating composition while keeping the first patch small. When unevaluatedProperties is a schema, each remaining property is validated against it. When it is false, the validator reports the property with a dedicated constraint error.

The validator does not yet carry annotations across every applicator, so this intentionally does not claim complete Draft 2019-09 semantics for anyOf, oneOf, conditionals, or referenced schemas. I would treat this as a proof of concept for the project to shape further.

Tests

  • php -d extension=mbstring -d extension=openssl vendor/bin/phpunit --filter UnevaluatedPropertiesTest --testdox passes locally (2 assertions-backed cases, 2 expected skips for the assoc/type-cast variants).
  • PHPStan passes for the new constraint.
  • git diff --check passes.
  • The pre-existing full-suite run reaches the suite but has an unrelated Windows path-separator failure; the original CI run also exposed the missing Draft 2019 strict-mode flag in this new test, which is fixed in the follow-up commit.

This patch was prepared with AI assistance and then reviewed and tested in this checkout. The repository's contribution notice asks contributors to have authored 100% of the content, so please let me know if the project needs a different treatment of that requirement before considering the patch.

Updates #907

@DannyvdSluijs

Copy link
Copy Markdown
Collaborator

@tomatotomata ill be back from
Holidays next week. Hoping I can provide a review late next week. Early quick scan already enabled me to start the workflows which have been picked up by you already. Thnx.

@DannyvdSluijs DannyvdSluijs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice work. I didn't think about this solution direction. Refreshing to see your approach.
I did add some review comments could you perhaps take a look and see if you can contribute?

use JsonSchema\Entity\JsonPointer;

/**
* Proof-of-concept support for unevaluatedProperties.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Before merging we should remove the proof on concept comment.

Suggested change
* Proof-of-concept support for unevaluatedProperties.

Comment on lines +15 to +17
* The current validator does not carry annotations between applicators, so this
* first implementation derives the evaluated property names from properties,
* patternProperties, and allOf branches in the current schema.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The comment doesn't need to inform about this being the first implementation

Suggested change
* The current validator does not carry annotations between applicators, so this
* first implementation derives the evaluated property names from properties,
* patternProperties, and allOf branches in the current schema.
* The current validator does not carry annotations between applicators, so this
* constraint derives the evaluated property names from properties,
* patternProperties, and allOf branches in the current schema.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: I think this file would belong in test/Constraints/Drafts/Draft2019/UnevaluatedPropertiesConstraintTest.php

Comment on lines +67 to +78
/**
* @param object $schema
* @param object $value
*
* @return array<int, string>
*/
private function collectEvaluatedProperties($schema, object $value): array
{
if (!is_object($schema)) {
return [];
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Since $object was already verified as being of type object on line 43 we can safely narrow the type of the function to object and skip the check as well as the doc types since we use the native typing. This requires a additional check on line 91 where we do a recursive call.

Suggested change
/**
* @param object $schema
* @param object $value
*
* @return array<int, string>
*/
private function collectEvaluatedProperties($schema, object $value): array
{
if (!is_object($schema)) {
return [];
}
/**
* @return array<int, string>
*/
private function collectEvaluatedProperties(object $schema, object $value): array
{

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: With this keyword now supported we should alos enable the test in

'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties schema: with invalid unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties false: with unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with adjacent properties: with unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with adjacent patternProperties: with unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with nested properties: with additional properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with nested patternProperties: with additional properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with anyOf: when one matches and has unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with anyOf: when two match and has unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with oneOf: with unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with not: with unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else: when if is true and has unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else: when if is false and has unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else, then not defined: when if is true and has no unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else, then not defined: when if is true and has unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else, then not defined: when if is false and has unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else, else not defined: when if is true and has unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else, else not defined: when if is false and has no unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with if/then/else, else not defined: when if is false and has unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with dependentSchemas: with unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with boolean schemas: with unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties with $ref: with unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties can\'t see inside cousins: always fails is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties can\'t see inside cousins (reverse order): always fails is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: nested unevaluatedProperties, outer true, inner false, properties outside: with no nested unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: nested unevaluatedProperties, outer true, inner false, properties outside: with nested unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: nested unevaluatedProperties, outer true, inner false, properties inside: with nested unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: cousin unevaluatedProperties, true and false, true with properties: with no nested unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: cousin unevaluatedProperties, true and false, true with properties: with nested unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: cousin unevaluatedProperties, true and false, false with properties: with nested unevaluated properties is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: property is evaluated in an uncle schema to unevaluatedProperties: uncle keyword evaluation is not significant is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: in-place applicator siblings, allOf has unevaluated: base case: both properties present is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: in-place applicator siblings, allOf has unevaluated: in place applicator siblings, foo is missing is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: in-place applicator siblings, anyOf has unevaluated: base case: both properties present is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: in-place applicator siblings, anyOf has unevaluated: in place applicator siblings, bar is missing is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties + single cyclic ref: Unevaluated on 1st level is invalid is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties + single cyclic ref: Unevaluated on 2nd level is invalid is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties + single cyclic ref: Unevaluated on 3rd level is invalid is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: dynamic evalation inside nested refs: xx + foo is invalid is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties not affected by propertyNames: string property is invalid is expected to be invalid',
'[draft2019-09/unevaluatedProperties.json]: unevaluatedProperties can see annotations from if without then and else: invalid in case if is evaluated is expected to be invalid',

if ($schema->unevaluatedProperties === true) {
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: This should also evaluate the availability and value of additionalProperties, see the tests in the test suite and the spec.

foreach ($schema->allOf as $branch) {
$evaluated = array_merge($evaluated, $this->collectEvaluatedProperties($branch, $value));
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: This should include (either now or at a later state) anyOf, oneOf, if/then/else and more keywords right? Curious to learn what would be your suggested approach?

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants