diff --git a/src/JsonSchema/ConstraintError.php b/src/JsonSchema/ConstraintError.php index 0934292c3..0268dd4cd 100644 --- a/src/JsonSchema/ConstraintError.php +++ b/src/JsonSchema/ConstraintError.php @@ -54,6 +54,7 @@ class ConstraintError extends Enum public const NOT = 'not'; public const ONE_OF = 'oneOf'; public const REQUIRED = 'required'; + public const UNRESOLVABLE_REF = 'unresolvableRef'; public const REQUIRES = 'requires'; public const PATTERN = 'pattern'; public const PREGEX_INVALID = 'pregrex'; @@ -118,6 +119,7 @@ public function getMessage() self::NOT => 'Matched a schema which it should not', self::ONE_OF => 'Failed to match exactly one schema', self::REQUIRED => 'The property %s is required', + self::UNRESOLVABLE_REF => 'The $ref "%s" could not be resolved', self::REQUIRES => 'The presence of the property %s requires that %s also be present', self::PATTERN => 'Does not match the regex pattern %s', self::PREGEX_INVALID => 'The pattern %s is invalid', diff --git a/src/JsonSchema/Constraints/Drafts/Draft06/RefConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft06/RefConstraint.php index f52bcda0a..e1e6c55f1 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft06/RefConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft06/RefConstraint.php @@ -4,6 +4,7 @@ namespace JsonSchema\Constraints\Drafts\Draft06; +use JsonSchema\ConstraintError; use JsonSchema\Constraints\ConstraintInterface; use JsonSchema\Entity\ErrorBagProxy; use JsonSchema\Entity\JsonPointer; @@ -30,6 +31,10 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n try { $refSchema = $this->factory->getSchemaStorage()->resolveRefSchema($schema); } catch (\Exception $e) { + // A $ref that cannot be resolved is a broken schema, not an absent keyword: + // returning here would let the value pass as if the $ref had not been written. + $this->addError(ConstraintError::UNRESOLVABLE_REF(), $path, ['ref' => $schema->{'$ref'}]); + return; } diff --git a/src/JsonSchema/Constraints/Drafts/Draft07/RefConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft07/RefConstraint.php index 9ec6efdac..9913d10e3 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft07/RefConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft07/RefConstraint.php @@ -4,6 +4,7 @@ namespace JsonSchema\Constraints\Drafts\Draft07; +use JsonSchema\ConstraintError; use JsonSchema\Constraints\ConstraintInterface; use JsonSchema\Entity\ErrorBagProxy; use JsonSchema\Entity\JsonPointer; @@ -30,6 +31,10 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n try { $refSchema = $this->factory->getSchemaStorage()->resolveRefSchema($schema); } catch (\Exception $e) { + // A $ref that cannot be resolved is a broken schema, not an absent keyword: + // returning here would let the value pass as if the $ref had not been written. + $this->addError(ConstraintError::UNRESOLVABLE_REF(), $path, ['ref' => $schema->{'$ref'}]); + return; } diff --git a/src/JsonSchema/Constraints/Drafts/Draft2019/RefConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft2019/RefConstraint.php index 989b1f039..a095234c0 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft2019/RefConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft2019/RefConstraint.php @@ -4,6 +4,7 @@ namespace JsonSchema\Constraints\Drafts\Draft2019; +use JsonSchema\ConstraintError; use JsonSchema\Constraints\ConstraintInterface; use JsonSchema\Entity\ErrorBagProxy; use JsonSchema\Entity\JsonPointer; @@ -30,6 +31,10 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n try { $refSchema = $this->factory->getSchemaStorage()->resolveRefSchema($schema); } catch (\Exception $e) { + // A $ref that cannot be resolved is a broken schema, not an absent keyword: + // returning here would let the value pass as if the $ref had not been written. + $this->addError(ConstraintError::UNRESOLVABLE_REF(), $path, ['ref' => $schema->{'$ref'}]); + return; } diff --git a/tests/Constraints/Draft06/RefConstraintTest.php b/tests/Constraints/Draft06/RefConstraintTest.php new file mode 100644 index 000000000..94ce3d6ee --- /dev/null +++ b/tests/Constraints/Draft06/RefConstraintTest.php @@ -0,0 +1,42 @@ +validate($data, $schema, Constraint::CHECK_MODE_STRICT); + + self::assertFalse($validator->isValid()); + $errors = $validator->getErrors(); + self::assertCount(1, $errors); + self::assertSame('unresolvableRef', $errors[0]['constraint']['name']); + } + + public function testAResolvableRefStillValidates(): void + { + $validator = new Validator(); + $data = json_decode('{"child": 1}'); + $schema = json_decode( + '{"$schema": "' . self::DIALECT . '", "$defs": {"num": {"type": "integer"}},' + . ' "properties": {"child": {"$ref": "#/$defs/num"}}}' + ); + + $validator->validate($data, $schema, Constraint::CHECK_MODE_STRICT); + + self::assertTrue($validator->isValid(), (string) json_encode($validator->getErrors())); + } +} diff --git a/tests/Constraints/Draft07/RefConstraintTest.php b/tests/Constraints/Draft07/RefConstraintTest.php new file mode 100644 index 000000000..68c14c7b3 --- /dev/null +++ b/tests/Constraints/Draft07/RefConstraintTest.php @@ -0,0 +1,42 @@ +validate($data, $schema, Constraint::CHECK_MODE_STRICT); + + self::assertFalse($validator->isValid()); + $errors = $validator->getErrors(); + self::assertCount(1, $errors); + self::assertSame('unresolvableRef', $errors[0]['constraint']['name']); + } + + public function testAResolvableRefStillValidates(): void + { + $validator = new Validator(); + $data = json_decode('{"child": 1}'); + $schema = json_decode( + '{"$schema": "' . self::DIALECT . '", "$defs": {"num": {"type": "integer"}},' + . ' "properties": {"child": {"$ref": "#/$defs/num"}}}' + ); + + $validator->validate($data, $schema, Constraint::CHECK_MODE_STRICT); + + self::assertTrue($validator->isValid(), (string) json_encode($validator->getErrors())); + } +} diff --git a/tests/Constraints/Draft2019/RefConstraintTest.php b/tests/Constraints/Draft2019/RefConstraintTest.php new file mode 100644 index 000000000..9c1867fe4 --- /dev/null +++ b/tests/Constraints/Draft2019/RefConstraintTest.php @@ -0,0 +1,42 @@ +validate($data, $schema, Constraint::CHECK_MODE_STRICT); + + self::assertFalse($validator->isValid()); + $errors = $validator->getErrors(); + self::assertCount(1, $errors); + self::assertSame('unresolvableRef', $errors[0]['constraint']['name']); + } + + public function testAResolvableRefStillValidates(): void + { + $validator = new Validator(); + $data = json_decode('{"child": 1}'); + $schema = json_decode( + '{"$schema": "' . self::DIALECT . '", "$defs": {"num": {"type": "integer"}},' + . ' "properties": {"child": {"$ref": "#/$defs/num"}}}' + ); + + $validator->validate($data, $schema, Constraint::CHECK_MODE_STRICT); + + self::assertTrue($validator->isValid(), (string) json_encode($validator->getErrors())); + } +}