diff --git a/CHANGELOG.md b/CHANGELOG.md
index c346c4f4..088d1c76 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Update test case to current (PHP) standards ([#831](https://github.com/jsonrainbow/json-schema/pull/831))
- Upgrade test suite to use generators ([#834](https://github.com/jsonrainbow/json-schema/pull/834))
+- update to latest json schema test suite ([#821](https://github.com/jsonrainbow/json-schema/pull/821))
## [6.4.2] - 2025-06-03
### Fixed
diff --git a/composer.json b/composer.json
index 8229d635..e8e847e6 100644
--- a/composer.json
+++ b/composer.json
@@ -33,7 +33,7 @@
},
"require-dev": {
"friendsofphp/php-cs-fixer": "3.3.0",
- "json-schema/json-schema-test-suite": "1.2.0",
+ "json-schema/json-schema-test-suite": "^23.2",
"phpunit/phpunit": "^8.5",
"phpspec/prophecy": "^1.19",
"phpstan/phpstan": "^1.12",
@@ -59,11 +59,11 @@
"type": "package",
"package": {
"name": "json-schema/json-schema-test-suite",
- "version": "1.2.0",
+ "version": "23.2.0",
"source": {
"type": "git",
"url": "https://github.com/json-schema/JSON-Schema-Test-Suite",
- "reference": "1.2.0"
+ "reference": "23.2.0"
}
}
}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index f49455f2..bcae9ec7 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -24,4 +24,8 @@
./src/JsonSchema/
+
+
+
+
diff --git a/tests/Constraints/BaseTestCase.php b/tests/Constraints/BaseTestCase.php
index f65037d6..63b63a42 100644
--- a/tests/Constraints/BaseTestCase.php
+++ b/tests/Constraints/BaseTestCase.php
@@ -19,7 +19,7 @@ abstract class BaseTestCase extends VeryBaseTestCase
/**
* @dataProvider getInvalidTests
*
- * @param int-mask-of $checkMode
+ * @param ?int-mask-of $checkMode
*/
public function testInvalidCases(string $input, string $schema, ?int $checkMode = Constraint::CHECK_MODE_NORMAL, array $errors = []): void
{
@@ -28,8 +28,9 @@ public function testInvalidCases(string $input, string $schema, ?int $checkMode
$checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA;
}
- $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema, false)));
- $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json');
+ $schema = json_decode($schema, false);
+ $schemaStorage = new SchemaStorage($this->getUriRetrieverMock($schema));
+ $schema = $schemaStorage->getSchema($schema->id ?? 'http://www.my-domain.com/schema.json');
if (is_object($schema) && !isset($schema->{'$schema'})) {
$schema->{'$schema'} = $this->schemaSpec;
}
@@ -38,7 +39,7 @@ public function testInvalidCases(string $input, string $schema, ?int $checkMode
$checkValue = json_decode($input, false);
$errorMask = $validator->validate($checkValue, $schema);
- $this->assertTrue((bool) ($errorMask & Validator::ERROR_DOCUMENT_VALIDATION));
+ $this->assertTrue((bool) ($errorMask & Validator::ERROR_DOCUMENT_VALIDATION), 'Document is invalid');
$this->assertGreaterThan(0, $validator->numErrors());
if ([] !== $errors) {
@@ -49,8 +50,10 @@ public function testInvalidCases(string $input, string $schema, ?int $checkMode
/**
* @dataProvider getInvalidForAssocTests
+ *
+ * @param ?int-mask-of $checkMode
*/
- public function testInvalidCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_TYPE_CAST, $errors = []): void
+ public function testInvalidCasesUsingAssoc(string $input, string $schema, ?int $checkMode = Constraint::CHECK_MODE_TYPE_CAST, array $errors = []): void
{
$checkMode = $checkMode ?? Constraint::CHECK_MODE_TYPE_CAST;
if ($this->validateSchema) {
@@ -60,8 +63,9 @@ public function testInvalidCasesUsingAssoc($input, $schema, $checkMode = Constra
$this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_TYPE_CAST"');
}
- $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema)));
- $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json');
+ $schema = json_decode($schema, false);
+ $schemaStorage = new SchemaStorage($this->getUriRetrieverMock($schema));
+ $schema = $schemaStorage->getSchema($schema->id ?? 'http://www.my-domain.com/schema.json');
if (is_object($schema) && !isset($schema->{'$schema'})) {
$schema->{'$schema'} = $this->schemaSpec;
}
@@ -81,14 +85,18 @@ public function testInvalidCasesUsingAssoc($input, $schema, $checkMode = Constra
/**
* @dataProvider getValidTests
+ *
+ * @param ?int-mask-of $checkMode
*/
- public function testValidCases($input, $schema, $checkMode = Constraint::CHECK_MODE_NORMAL): void
+ public function testValidCases(string $input, string $schema, ?int $checkMode = Constraint::CHECK_MODE_NORMAL): void
{
if ($this->validateSchema) {
$checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA;
}
- $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema, false)));
- $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json');
+
+ $schema = json_decode($schema, false);
+ $schemaStorage = new SchemaStorage($this->getUriRetrieverMock($schema));
+ $schema = $schemaStorage->getSchema($schema->id ?? 'http://www.my-domain.com/schema.json');
if (is_object($schema) && !isset($schema->{'$schema'})) {
$schema->{'$schema'} = $this->schemaSpec;
}
@@ -103,8 +111,10 @@ public function testValidCases($input, $schema, $checkMode = Constraint::CHECK_M
/**
* @dataProvider getValidForAssocTests
+ *
+ * @param ?int-mask-of $checkMode
*/
- public function testValidCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_TYPE_CAST): void
+ public function testValidCasesUsingAssoc(string $input, string $schema, ?int $checkMode = Constraint::CHECK_MODE_TYPE_CAST): void
{
if ($this->validateSchema) {
$checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA;
@@ -113,9 +123,9 @@ public function testValidCasesUsingAssoc($input, $schema, $checkMode = Constrain
$this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_TYPE_CAST"');
}
- $schema = json_decode($schema);
+ $schema = json_decode($schema, false);
$schemaStorage = new SchemaStorage($this->getUriRetrieverMock($schema), new UriResolver());
- $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json');
+ $schema = $schemaStorage->getSchema($schema->id ?? 'http://www.my-domain.com/schema.json');
if (is_object($schema) && !isset($schema->{'$schema'})) {
$schema->{'$schema'} = $this->schemaSpec;
}
@@ -124,7 +134,7 @@ public function testValidCasesUsingAssoc($input, $schema, $checkMode = Constrain
$validator = new Validator(new Factory($schemaStorage, null, $checkMode));
$errorMask = $validator->validate($value, $schema);
- $this->assertEquals(0, $errorMask);
+ $this->assertEquals(0, $errorMask, $this->validatorErrorsToString($validator));
$this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true));
}
@@ -141,4 +151,14 @@ public function getInvalidForAssocTests(): Generator
{
yield from $this->getInvalidTests();
}
+
+ private function validatorErrorsToString(Validator $validator): string
+ {
+ return implode(
+ ', ',
+ array_map(
+ static function (array $error) { return $error['message']; }, $validator->getErrors()
+ )
+ );
+ }
}
diff --git a/tests/Constraints/NumberAndIntegerTypesTest.php b/tests/Constraints/NumberAndIntegerTypesTest.php
index c09abee8..7469dd9a 100644
--- a/tests/Constraints/NumberAndIntegerTypesTest.php
+++ b/tests/Constraints/NumberAndIntegerTypesTest.php
@@ -12,10 +12,8 @@ class NumberAndIntegerTypesTest extends BaseTestCase
public function getInvalidTests(): \Generator
{
yield [
- '{
- "integer": 1.4
- }',
- '{
+ 'input' => '{ "integer": 1.4 }',
+ 'schema' => '{
"type":"object",
"properties":{
"integer":{"type":"integer"}
@@ -23,8 +21,8 @@ public function getInvalidTests(): \Generator
}'
];
yield [
- '{"integer": 1.001}',
- '{
+ 'input' => '{"integer": 1.001}',
+ 'schema' => '{
"type": "object",
"properties": {
"integer": {"type": "integer"}
@@ -32,8 +30,8 @@ public function getInvalidTests(): \Generator
}'
];
yield [
- '{"integer": true}',
- '{
+ 'input' => '{"integer": true}',
+ 'schema' => '{
"type": "object",
"properties": {
"integer": {"type": "integer"}
@@ -41,8 +39,8 @@ public function getInvalidTests(): \Generator
}'
];
yield [
- '{"number": "x"}',
- '{
+ 'input' => '{"number": "x"}',
+ 'schema' => '{
"type": "object",
"properties": {
"number": {"type": "number"}
@@ -54,10 +52,8 @@ public function getInvalidTests(): \Generator
public function getValidTests(): \Generator
{
yield [
- '{
- "integer": 1
- }',
- '{
+ 'input' => '{ "integer": 1 }',
+ 'schema' => '{
"type":"object",
"properties":{
"integer":{"type":"integer"}
@@ -65,10 +61,8 @@ public function getValidTests(): \Generator
}'
];
yield [
- '{
- "number": 1.4
- }',
- '{
+ 'input' => '{ "number": 1.4 }',
+ 'schema' => '{
"type":"object",
"properties":{
"number":{"type":"number"}
@@ -76,8 +70,8 @@ public function getValidTests(): \Generator
}'
];
yield [
- '{"number": 1e5}',
- '{
+ 'input' => '{"number": 1e5}',
+ 'schema' => '{
"type": "object",
"properties": {
"number": {"type": "number"}
@@ -85,8 +79,8 @@ public function getValidTests(): \Generator
}'
];
yield [
- '{"number": 1}',
- '{
+ 'input' => '{"number": 1}',
+ 'schema' => '{
"type": "object",
"properties": {
"number": {"type": "number"}
@@ -95,8 +89,8 @@ public function getValidTests(): \Generator
}'
];
yield [
- '{"number": -49.89}',
- '{
+ 'input' => '{"number": -49.89}',
+ 'schema' => '{
"type": "object",
"properties": {
"number": {
diff --git a/tests/Constraints/PatternPropertiesTest.php b/tests/Constraints/PatternPropertiesTest.php
index ca99fc47..a4042602 100644
--- a/tests/Constraints/PatternPropertiesTest.php
+++ b/tests/Constraints/PatternPropertiesTest.php
@@ -79,8 +79,8 @@ public function getInvalidTests(): \Generator
public function getValidTests(): \Generator
{
- [
- yield 'validates pattern schema' => json_encode([
+ yield 'validates pattern schema' => [
+ json_encode([
'someobject' => [
'foobar' => 'foo',
'barfoo' => 'bar',
diff --git a/tests/Constraints/VeryBaseTestCase.php b/tests/Constraints/VeryBaseTestCase.php
index 55d0672d..f468ac0b 100644
--- a/tests/Constraints/VeryBaseTestCase.php
+++ b/tests/Constraints/VeryBaseTestCase.php
@@ -20,7 +20,7 @@ abstract class VeryBaseTestCase extends TestCase
protected function getUriRetrieverMock(?object $schema): object
{
$uriRetriever = $this->prophesize(UriRetrieverInterface::class);
- $uriRetriever->retrieve('http://www.my-domain.com/schema.json')
+ $uriRetriever->retrieve($schema->id ?? 'http://www.my-domain.com/schema.json')
->willReturn($schema)
->shouldBeCalled();
@@ -71,4 +71,9 @@ private function readAndJsonDecodeFile(string $file): stdClass
return json_decode(file_get_contents($file), false);
}
+
+ protected function is32Bit(): bool
+ {
+ return PHP_INT_SIZE === 4;
+ }
}
diff --git a/tests/Drafts/Draft3Test.php b/tests/Drafts/Draft3Test.php
index 4ed6f76c..2d1202e7 100644
--- a/tests/Drafts/Draft3Test.php
+++ b/tests/Drafts/Draft3Test.php
@@ -75,6 +75,20 @@ protected function getFilePaths(): array
];
}
+ public function getInvalidTests(): \Generator
+ {
+ $skip = [
+ 'ref.json / $ref prevents a sibling id from changing the base uri / $ref resolves to /definitions/base_foo, data does not validate'
+ ];
+
+ foreach (parent::getInvalidTests() as $name => $testcase) {
+ if (in_array($name, $skip, true)) {
+ continue;
+ }
+ yield $name => $testcase;
+ }
+ }
+
public function getInvalidForAssocTests(): \Generator
{
$skip = [
@@ -113,6 +127,7 @@ protected function getSkippedTests(): array
return [
// Optional
'bignum.json',
+ 'ecmascript-regex.json',
'format.json',
'jsregex.json',
'zeroTerminatedFloats.json'
diff --git a/tests/Drafts/Draft4Test.php b/tests/Drafts/Draft4Test.php
index dbf1354a..4cd0c865 100644
--- a/tests/Drafts/Draft4Test.php
+++ b/tests/Drafts/Draft4Test.php
@@ -20,9 +20,33 @@ protected function getFilePaths(): array
];
}
+ public function getInvalidTests(): \Generator
+ {
+ $skip = [
+ 'id.json / id inside an enum is not a real identifier / no match on enum or $ref to id',
+ 'ref.json / $ref prevents a sibling id from changing the base uri / $ref resolves to /definitions/base_foo, data does not validate',
+ 'ref.json / Recursive references between schemas / invalid tree',
+ 'ref.json / refs with quote / object with strings is invalid',
+ 'ref.json / Location-independent identifier / mismatch',
+ 'ref.json / Location-independent identifier with base URI change in subschema / mismatch',
+ 'ref.json / empty tokens in $ref json-pointer / non-number is invalid',
+ 'ref.json / id must be resolved against nearest parent, not just immediate parent / non-number is invalid',
+ 'refRemote.json / Location-independent identifier in remote ref / string is invalid',
+ 'refRemote.json / base URI change - change folder / string is invalid'
+ ];
+
+ foreach (parent::getInvalidTests() as $name => $testcase) {
+ if (in_array($name, $skip, true)) {
+ continue;
+ }
+ yield $name => $testcase;
+ }
+ }
+
public function getInvalidForAssocTests(): \Generator
{
$skip = [
+ 'ref.json / Recursive references between schemas / valid tree',
'type.json / object type matches objects / an array is not an object',
'type.json / array type matches arrays / an object is not an array',
];
@@ -35,9 +59,39 @@ public function getInvalidForAssocTests(): \Generator
}
}
+ public function getValidTests(): \Generator
+ {
+ $skip = [
+ 'ref.json / $ref prevents a sibling id from changing the base uri / $ref resolves to /definitions/base_foo, data validates',
+ 'ref.json / Recursive references between schemas / valid tree',
+ 'ref.json / refs with quote / object with numbers is valid',
+ 'ref.json / Location-independent identifier / match',
+ 'ref.json / Location-independent identifier with base URI change in subschema / match',
+ 'ref.json / empty tokens in $ref json-pointer / number is valid',
+ 'ref.json / naive replacement of $ref with its destination is not correct / match the enum exactly',
+ 'ref.json / id must be resolved against nearest parent, not just immediate parent / number is valid',
+ 'refRemote.json / Location-independent identifier in remote ref / integer is valid',
+ 'refRemote.json / base URI change - change folder / number is valid',
+ ];
+
+ if ($this->is32Bit()) {
+ $skip[] = 'multipleOf.json / small multiple of large integer / any integer is a multiple of 1e-8'; // Test case contains a number which doesn't fit in 32 bits
+ }
+
+ foreach (parent::getValidTests() as $name => $testcase) {
+ if (in_array($name, $skip, true)) {
+ continue;
+ }
+ yield $name => $testcase;
+ }
+ }
+
public function getValidForAssocTests(): \Generator
{
$skip = [
+ 'minProperties.json / minProperties validation / ignores arrays',
+ 'required.json / required properties whose names are Javascript object property names / ignores arrays',
+ 'required.json / required validation / ignores arrays',
'type.json / object type matches objects / an array is not an object',
'type.json / array type matches arrays / an object is not an array',
];
@@ -58,7 +112,9 @@ protected function getSkippedTests(): array
return [
// Optional
'bignum.json',
+ 'ecmascript-regex.json',
'format.json',
+ 'float-overflow.json',
'zeroTerminatedFloats.json',
// Required
'not.json' // only one test case failing
diff --git a/tests/JsonSchemaTestSuiteTest.php b/tests/JsonSchemaTestSuiteTest.php
new file mode 100644
index 00000000..0c4931bf
--- /dev/null
+++ b/tests/JsonSchemaTestSuiteTest.php
@@ -0,0 +1,157 @@
+addSchema(property_exists($schema, 'id') ? $schema->id : SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI, $schema);
+ $this->loadRemotesIntoStorage($schemaStorage);
+ $validator = new Validator(new Factory($schemaStorage));
+
+ try {
+ $validator->validate($data, $schema);
+ } catch (\Exception $e) {
+ if ($optional) {
+ $this->markTestSkipped('Optional test case would during validate() invocation');
+ }
+
+ throw $e;
+ }
+
+ if ($optional && $expectedValidationResult !== (count($validator->getErrors()) === 0)) {
+ $this->markTestSkipped('Optional test case would fail');
+ }
+
+ self::assertEquals($expectedValidationResult, count($validator->getErrors()) === 0);
+ }
+
+ public function casesDataProvider(): \Generator
+ {
+ $testDir = __DIR__ . '/../vendor/json-schema/json-schema-test-suite/tests';
+ $drafts = array_filter(glob($testDir . '/*'), static function (string $filename) {
+ return is_dir($filename);
+ });
+ $skippedDrafts = ['draft6', 'draft7', 'draft2019-09', 'draft2020-12', 'draft-next', 'latest'];
+
+ foreach ($drafts as $draft) {
+ if (in_array(basename($draft), $skippedDrafts, true)) {
+ continue;
+ }
+
+ $files = new CallbackFilterIterator(
+ new RecursiveIteratorIterator(
+ new RecursiveDirectoryIterator($draft)
+ ),
+ function ($file) {
+ return $file->isFile() && strtolower($file->getExtension()) === 'json';
+ }
+ );
+ /** @var \SplFileInfo $file */
+ foreach ($files as $file) {
+ $contents = json_decode(file_get_contents($file->getPathname()), false);
+ foreach ($contents as $testCase) {
+ foreach ($testCase->tests as $test) {
+ $name = sprintf(
+ '[%s/%s%s]: %s: %s is expected to be %s',
+ basename($draft),
+ str_contains($file->getPathname(), '/optional/') ? 'optional/' : '',
+ $file->getBasename(),
+ $testCase->description,
+ $test->description,
+ $test->valid ? 'valid' : 'invalid'
+ );
+
+ if ($this->shouldNotYieldTest($name)) {
+ continue;
+ }
+
+ yield $name => [
+ 'testCaseDescription' => $testCase->description,
+ 'testDescription' => $test->description,
+ 'schema' => $testCase->schema,
+ 'data' => $test->data,
+ 'expectedValidationResult' => $test->valid,
+ 'optional' => str_contains($file->getPathname(), '/optional/')
+ ];
+ }
+
+ }
+ }
+ }
+ }
+
+ private function loadRemotesIntoStorage(SchemaStorageInterface $storage): void
+ {
+ $remotesDir = __DIR__ . '/../vendor/json-schema/json-schema-test-suite/remotes';
+
+ $directory = new \RecursiveDirectoryIterator($remotesDir);
+ $iterator = new \RecursiveIteratorIterator($directory);
+
+ foreach ($iterator as $info) {
+ if (!$info->isFile()) {
+ continue;
+ }
+
+ $id = str_replace($remotesDir, 'http://localhost:1234', $info->getPathname());
+ $storage->addSchema($id, json_decode(file_get_contents($info->getPathname()), false));
+ }
+ }
+
+ private function shouldNotYieldTest(string $name): bool
+ {
+ $skip = [
+ '[draft4/ref.json]: refs with quote: object with numbers is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now.
+ '[draft4/ref.json]: refs with quote: object with strings is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now.
+ '[draft4/ref.json]: Location-independent identifier: match is expected to be valid', // Test case was added after v1.2.0, skip test for now.
+ '[draft4/ref.json]: Location-independent identifier: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now.
+ '[draft4/ref.json]: Location-independent identifier with base URI change in subschema: match is expected to be valid', // Test case was added after v1.2.0, skip test for now.
+ '[draft4/ref.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now.
+ '[draft4/ref.json]: id must be resolved against nearest parent, not just immediate parent: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now.
+ '[draft4/ref.json]: id must be resolved against nearest parent, not just immediate parent: non-number is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now.
+ '[draft4/ref.json]: empty tokens in $ref json-pointer: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now.
+ '[draft4/ref.json]: empty tokens in $ref json-pointer: non-number is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now.
+ '[draft4/refRemote.json]: base URI change - change folder: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now.
+ '[draft4/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now.
+ '[draft4/refRemote.json]: Location-independent identifier in remote ref: integer is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now.
+ '[draft4/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now.
+ ];
+
+ if ($this->is32Bit()) {
+ $skip[] = '[draft4/multipleOf.json]: small multiple of large integer: any integer is a multiple of 1e-8 is expected to be valid'; // Test case contains a number which doesn't fit in 32 bits
+ }
+
+ return in_array($name, $skip, true);
+ }
+
+ private function is32Bit(): bool
+ {
+ return PHP_INT_SIZE === 4;
+ }
+
+}