Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/JsonSchema/ConstraintError.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
Expand Down
5 changes: 5 additions & 0 deletions src/JsonSchema/Constraints/Drafts/Draft06/RefConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace JsonSchema\Constraints\Drafts\Draft06;

use JsonSchema\ConstraintError;
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;
Expand All @@ -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'}]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed, measured on all three drafts in strict mode: anyOf [$ref, {}], oneOf [$ref, {}] and not $ref all come out valid with an unresolvable $ref. I laid out the options in the main thread, since the fix changes the approach the PR was approved with.


return;
}

Expand Down
5 changes: 5 additions & 0 deletions src/JsonSchema/Constraints/Drafts/Draft07/RefConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace JsonSchema\Constraints\Drafts\Draft07;

use JsonSchema\ConstraintError;
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;
Expand All @@ -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'}]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed, measured on all three drafts in strict mode: anyOf [$ref, {}], oneOf [$ref, {}] and not $ref all come out valid with an unresolvable $ref. I laid out the options in the main thread, since the fix changes the approach the PR was approved with.


return;
}

Expand Down
5 changes: 5 additions & 0 deletions src/JsonSchema/Constraints/Drafts/Draft2019/RefConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace JsonSchema\Constraints\Drafts\Draft2019;

use JsonSchema\ConstraintError;
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;
Expand All @@ -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'}]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed, measured on all three drafts in strict mode: anyOf [$ref, {}], oneOf [$ref, {}] and not $ref all come out valid with an unresolvable $ref. I laid out the options in the main thread, since the fix changes the approach the PR was approved with.


return;
}

Expand Down
42 changes: 42 additions & 0 deletions tests/Constraints/Draft06/RefConstraintTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace JsonSchema\Tests\Constraints\Draft06;

use JsonSchema\Constraints\Constraint;
use JsonSchema\Tests\Constraints\VeryBaseTestCase;
use JsonSchema\Validator;

class RefConstraintTest extends VeryBaseTestCase
{
private const DIALECT = 'http://json-schema.org/draft-06/schema#';

public function testAnUnresolvableRefIsReportedInsteadOfSilentlyPassing(): void
{
$validator = new Validator();
$data = new \stdClass();
$schema = json_decode('{"$schema": "' . self::DIALECT . '", "$ref": "#/$defs/nonExistent"}');

$validator->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()));
}
}
42 changes: 42 additions & 0 deletions tests/Constraints/Draft07/RefConstraintTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace JsonSchema\Tests\Constraints\Draft07;

use JsonSchema\Constraints\Constraint;
use JsonSchema\Tests\Constraints\VeryBaseTestCase;
use JsonSchema\Validator;

class RefConstraintTest extends VeryBaseTestCase
{
private const DIALECT = 'http://json-schema.org/draft-07/schema#';

public function testAnUnresolvableRefIsReportedInsteadOfSilentlyPassing(): void
{
$validator = new Validator();
$data = new \stdClass();
$schema = json_decode('{"$schema": "' . self::DIALECT . '", "$ref": "#/$defs/nonExistent"}');

$validator->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()));
}
}
42 changes: 42 additions & 0 deletions tests/Constraints/Draft2019/RefConstraintTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace JsonSchema\Tests\Constraints\Draft2019;

use JsonSchema\Constraints\Constraint;
use JsonSchema\Tests\Constraints\VeryBaseTestCase;
use JsonSchema\Validator;

class RefConstraintTest extends VeryBaseTestCase
{
private const DIALECT = 'https://json-schema.org/draft/2019-09/schema';

public function testAnUnresolvableRefIsReportedInsteadOfSilentlyPassing(): void
{
$validator = new Validator();
$data = new \stdClass();
$schema = json_decode('{"$schema": "' . self::DIALECT . '", "$ref": "#/$defs/nonExistent"}');

$validator->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()));
}
}
Loading