Skip to content

Avoid unconditional constraint instantiation in Draft06/07/2019 dispatch - #926

Open
DannyvdSluijs wants to merge 1 commit into
mainfrom
fix/920-avoid-unconditional-constraint-instantiation
Open

Avoid unconditional constraint instantiation in Draft06/07/2019 dispatch#926
DannyvdSluijs wants to merge 1 commit into
mainfrom
fix/920-avoid-unconditional-constraint-instantiation

Conversation

@DannyvdSluijs

Copy link
Copy Markdown
Collaborator

Summary

  • Draft06Constraint, Draft07Constraint, and Draft2019Constraint previously called checkForKeyword() for every registered keyword on every schema node, unconditionally instantiating a constraint object (via Factory::createInstanceFor()) even when the keyword was absent from the schema — the constraint's own check() would just immediately return via its internal property_exists($schema, ...) guard.
  • checkForKeyword() now skips createInstanceFor() entirely when the relevant schema property is absent, using a small per-class keyword→property map to handle the three keywords whose name doesn't match the schema property they guard on (ref$ref, ifThenElseif, contentcontentMediaType/contentEncoding).
  • No behavior change: the guard exactly mirrors each constraint's own existing early-return.

Fixes #920.

Verification

  • Full official JSON-Schema-Test-Suite (composer test): identical failure set before and after (verified via git-stash A/B diff of failing test names) — zero regressions.
  • composer phpstan (level 8): clean.
  • composer style-check: clean.
  • 16 new unit tests (tests/Constraints/Drafts/Draft{06,07,2019}/) assert createInstanceFor is skipped for absent keywords and invoked for present ones.
  • Benchmarked on a representative nested schema: constraint instantiations for one recursive validation dropped from 40 to 9; isolated dispatch throughput went from ~267 to ~864 checks/sec (~3.2x). No measurable change in peak memory — the avoided objects were already being freed immediately via refcounting, so this is a CPU/allocator-churn win rather than a footprint win.

Follow-up

A related, larger idea came up during review: gating could go further by also checking the value's type (not just schema-property presence), since ~76% of Draft2019 constraints already guard on value type internally. That's a bigger, potentially breaking change (touches ~98 constraint classes, would need an opt-in addition to ConstraintInterface to stay backward compatible for consumers using Factory::setConstraintClass()), so it's tracked separately in #925 rather than folded into this PR.

Test plan

  • composer test
  • composer phpstan
  • composer style-check

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR optimizes Draft06/Draft07/Draft2019 keyword dispatch by avoiding unconditional constraint instantiation when the corresponding schema keyword/property is absent, while preserving existing behavior (matching the constraints’ own early-return guards).

Changes:

  • Added per-draft keyword→schema-property maps for the few keywords whose dispatch name doesn’t match their guarding schema property (ref, ifThenElse, content).
  • Updated checkForKeyword() in Draft06/07/2019 dispatchers to skip Factory::createInstanceFor() unless the relevant schema property exists.
  • Added unit tests asserting that only present keywords trigger createInstanceFor() calls.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/JsonSchema/Constraints/Drafts/Draft06/Draft06Constraint.php Gate constraint instantiation on schema-property presence (incl. $ref mapping).
src/JsonSchema/Constraints/Drafts/Draft07/Draft07Constraint.php Gate constraint instantiation on schema-property presence (incl. $ref, if, content* mappings).
src/JsonSchema/Constraints/Drafts/Draft2019/Draft2019Constraint.php Gate constraint instantiation on schema-property presence (incl. $ref, if, content* mappings).
tests/Constraints/Drafts/Draft06/Draft06ConstraintTest.php New tests verifying instantiation is skipped for absent keywords in Draft06.
tests/Constraints/Drafts/Draft07/Draft07ConstraintTest.php New tests verifying instantiation is skipped for absent keywords (incl. special mappings) in Draft07.
tests/Constraints/Drafts/Draft2019/Draft2019ConstraintTest.php New tests verifying instantiation is skipped for absent keywords (incl. special mappings) in Draft2019.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +90 to +101
public function testDependentSchemasAndDependentRequiredAreInstantiatedIndependently(): void
{
$constraint = new Draft2019Constraint();
$calledKeywords = [];
$this->injectFactory($constraint, $this->createSpyFactory($calledKeywords));

$schema = json_decode('{"dependentRequired": {"a": ["b"]}}');
$value = json_decode('{"a": 1, "b": 2}');
$constraint->check($value, $schema);

$this->assertSame(['dependentRequired'], $calledKeywords);
}
checkForKeyword() now skips createInstanceFor() when the corresponding
schema property is absent, instead of always instantiating a constraint
object only for it to immediately no-op via its own internal guard.
Fixes #920.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@DannyvdSluijs
DannyvdSluijs force-pushed the fix/920-avoid-unconditional-constraint-instantiation branch from b379304 to 4f9cdb0 Compare July 24, 2026 09:57
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.

Draft06Constraint, Draft07Constraint, and Draft2019Constraint instantiate all constraint objects unconditionally per schema node

2 participants