From 6b5de369e87a4dc7d7167e0c9ff7ba600d278559 Mon Sep 17 00:00:00 2001 From: Danny van der Sluijs Date: Wed, 3 Jun 2026 21:57:31 +0200 Subject: [PATCH 1/5] fix: treat optional JSON Schema test suite cases as real tests (#913) Previously, any optional test that threw an exception or produced the wrong validation result was silently skipped via markTestSkipped, making regressions in optional test coverage invisible in CI. Remove the two dynamic markTestSkipped blocks so optional tests are treated identically to required tests. The 76 currently-failing optional tests are added explicitly to the $skip allowlist with comments explaining why each group is not yet supported (bignum, float-overflow, ecmascript-regex, cross-draft, idn-email, idn-hostname, iri, iri-reference, regex format, relative-json-pointer). Co-Authored-By: Claude Sonnet 4.6 --- tests/JsonSchemaTestSuiteTest.php | 103 +++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 16 deletions(-) diff --git a/tests/JsonSchemaTestSuiteTest.php b/tests/JsonSchemaTestSuiteTest.php index c6fc5565..68132ae4 100644 --- a/tests/JsonSchemaTestSuiteTest.php +++ b/tests/JsonSchemaTestSuiteTest.php @@ -30,8 +30,7 @@ public function testTestCaseValidatesCorrectly( $data, int $checkMode, DraftIdentifiers $draft, - bool $expectedValidationResult, - bool $optional + bool $expectedValidationResult ): void { $schemaStorage = new SchemaStorage(); $id = is_object($schema) && property_exists($schema, 'id') ? $schema->id : SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI; @@ -41,19 +40,7 @@ public function testTestCaseValidatesCorrectly( $factory->setDefaultDialect($draft->getValue()); $validator = new Validator($factory); - try { - $validator->validate($data, $schema, $checkMode); - } catch (\Exception $e) { - if ($optional) { - $this->markTestSkipped('Optional test case throws exception during validate() invocation: "' . $e->getMessage() . '"'); - } - - throw $e; - } - - if ($optional && $expectedValidationResult !== (count($validator->getErrors()) === 0)) { - $this->markTestSkipped('Optional test case would fail'); - } + $validator->validate($data, $schema, $checkMode); self::assertEquals( $expectedValidationResult, @@ -110,7 +97,6 @@ function ($file) { 'checkMode' => $this->getCheckModeForDraft($baseDraftName), 'draft' => DraftIdentifiers::fromConstraintName($baseDraftName), 'expectedValidationResult' => $test->valid, - 'optional' => str_contains($file->getPathname(), '/optional/') ]; } } @@ -192,6 +178,91 @@ private function shouldNotYieldTest(string $name): bool '[draft7/ref.json]: Location-independent identifier: mismatch is expected to be invalid', '[draft7/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', '[draft7/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', + // Optional: bignum — PHP does not natively support arbitrary-precision integers/floats + '[draft3/optional/bignum.json]: integer: a bignum is an integer is expected to be valid', + '[draft3/optional/bignum.json]: integer: a negative bignum is an integer is expected to be valid', + '[draft4/optional/bignum.json]: integer: a bignum is an integer is expected to be valid', + '[draft4/optional/bignum.json]: integer: a negative bignum is an integer is expected to be valid', + // Optional: float-overflow — PHP float precision differs from the ECMAScript model + '[draft4/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', + '[draft6/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', + '[draft7/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', + // Optional: ecmascript-regex — PHP uses PCRE which does not implement ECMAScript regex semantics + '[draft3/optional/ecmascript-regex.json]: ECMA 262 regex dialect recognition: [^] is a valid regex is expected to be valid', + '[draft3/optional/ecmascript-regex.json]: ECMA 262 regex dialect recognition: ECMA 262 has no support for lookbehind is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \D matches everything but ascii digits: NKO DIGIT ZERO (as \u escape) matches is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \D matches everything but ascii digits: NKO DIGIT ZERO matches (unlike e.g. Python) is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \S matches everything but whitespace: zero-width whitespace does not match is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \W matches everything but ascii letters: latin-1 e-acute matches (unlike e.g. Python) is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \d matches ascii digits only: NKO DIGIT ZERO (as \u escape) does not match is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \d matches ascii digits only: NKO DIGIT ZERO does not match (unlike e.g. Python) is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \s matches whitespace: zero-width whitespace matches is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: ECMA 262 \w matches ascii letters only: latin-1 e-acute does not match (unlike e.g. Python) is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \d in pattern matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \d in patternProperties matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \w in patterns matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: \w in patterns matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: pattern with non-ASCII digits: ascii digits is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: pattern with non-ASCII digits: ascii non-digits is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: pattern with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patternProperties with non-ASCII digits: ascii digits is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patternProperties with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: ascii character in json string is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: literal unicode character in json string is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: unicode character in hex format in string is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with pattern: unicode matching is case-sensitive is expected to be invalid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with patternProperties: ascii character in json string is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with patternProperties: literal unicode character in json string is expected to be valid', + '[draft4/optional/ecmascript-regex.json]: patterns always use unicode semantics with patternProperties: unicode character in hex format in string is expected to be valid', + '[draft6/optional/ecmascript-regex.json]: ECMA 262 \S matches everything but whitespace: zero-width whitespace does not match is expected to be invalid', + '[draft6/optional/ecmascript-regex.json]: ECMA 262 \s matches whitespace: zero-width whitespace matches is expected to be valid', + '[draft6/optional/ecmascript-regex.json]: \d in patternProperties matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', + '[draft6/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', + '[draft6/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', + '[draft6/optional/ecmascript-regex.json]: pattern with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', + '[draft7/optional/ecmascript-regex.json]: ECMA 262 \S matches everything but whitespace: zero-width whitespace does not match is expected to be invalid', + '[draft7/optional/ecmascript-regex.json]: ECMA 262 \s matches whitespace: zero-width whitespace matches is expected to be valid', + '[draft7/optional/ecmascript-regex.json]: \d in patternProperties matches [0-9], not unicode digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be invalid', + '[draft7/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: literal unicode character in json string is expected to be invalid', + '[draft7/optional/ecmascript-regex.json]: \w in patternProperties matches [A-Za-z0-9_], not unicode letters: unicode character in hex format in string is expected to be invalid', + '[draft7/optional/ecmascript-regex.json]: pattern with non-ASCII digits: non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO) is expected to be valid', + // Optional: cross-draft — cross-draft schema resolution ($ref across drafts) is not implemented + '[draft7/optional/cross-draft.json]: refs to future drafts are processed as future drafts: missing bar is invalid is expected to be invalid', + // Optional: idn-email — IDN e-mail format validation is not implemented + '[draft7/optional/format/idn-email.json]: validation of an internationalized e-mail addresses: an invalid e-mail address is expected to be invalid', + '[draft7/optional/format/idn-email.json]: validation of an internationalized e-mail addresses: an invalid idn e-mail address is expected to be invalid', + // Optional: idn-hostname — IDN hostname format validation is not implemented + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: Exceptions that are DISALLOWED, left-to-right chars is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: Exceptions that are DISALLOWED, right-to-left chars is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: KATAKANA MIDDLE DOT with no Hiragana, Katakana, or Han is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: KATAKANA MIDDLE DOT with no other characters is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with no following \'l\' is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with no preceding \'l\' is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with nothing following is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: MIDDLE DOT with nothing preceding is expected to be invalid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: ZERO WIDTH JOINER preceded by Virama is expected to be valid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: ZERO WIDTH NON-JOINER not preceded by Virama but matches regexp is expected to be valid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: ZERO WIDTH NON-JOINER preceded by Virama is expected to be valid', + '[draft7/optional/format/idn-hostname.json]: validation of internationalized host names: contains illegal char U+302E Hangul single dot tone mark is expected to be invalid', + // Optional: iri / iri-reference — IRI format validation is not implemented + '[draft7/optional/format/iri-reference.json]: validation of IRI References: an invalid IRI Reference is expected to be invalid', + '[draft7/optional/format/iri-reference.json]: validation of IRI References: an invalid IRI fragment is expected to be invalid', + '[draft7/optional/format/iri.json]: validation of IRIs: an invalid IRI based on IPv6 is expected to be invalid', + '[draft7/optional/format/iri.json]: validation of IRIs: an invalid IRI is expected to be invalid', + '[draft7/optional/format/iri.json]: validation of IRIs: an invalid IRI though valid IRI reference is expected to be invalid', + '[draft7/optional/format/iri.json]: validation of IRIs: an invalid relative IRI Reference is expected to be invalid', + // Optional: regex format — regex format validation does not check for valid ECMA-262 regex syntax + '[draft7/optional/format/regex.json]: validation of regular expressions: a regular expression with unclosed parens is invalid is expected to be invalid', + // Optional: relative-json-pointer — relative JSON pointer format validation is not implemented + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): ## is not a valid json-pointer is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): an invalid RJP that is a valid JSON Pointer is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): empty string is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): explicit positive prefix is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): negative prefix is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): zero cannot be followed by other digits, plus json-pointer is expected to be invalid', + '[draft7/optional/format/relative-json-pointer.json]: validation of Relative JSON Pointers (RJP): zero cannot be followed by other digits, plus octothorpe is expected to be invalid', ]; if ($this->is32Bit()) { From 3857303718062dbb58acc305e437bf45d5a04add Mon Sep 17 00:00:00 2001 From: Danny van der Sluijs Date: Thu, 4 Jun 2026 20:31:38 +0200 Subject: [PATCH 2/5] fix: Correct for +00:00 and zulu inputs --- .../Constraints/Drafts/Draft07/FormatConstraint.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php index 8bcd99f7..ade50215 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php @@ -40,7 +40,11 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n } break; case 'time': - if (!$this->validateDateTime($value, 'H:i:sp') && !$this->validateDateTime($value, 'H:i:s.up')) { + if (!$this->validateDateTime($value, 'H:i:sP') + && !$this->validateDateTime($value, 'H:i:sp') + && !$this->validateDateTime($value, 'H:i:s.up') + && !$this->validateDateTime($value, 'H:i:s.uP') + ) { $this->addError(ConstraintError::FORMAT_TIME(), $path, ['time' => $value, 'format' => $schema->format]); } break; From b4893783fe7d77e504691ffda54a90dc9f63ba24 Mon Sep 17 00:00:00 2001 From: Danny van der Sluijs Date: Thu, 4 Jun 2026 20:32:07 +0200 Subject: [PATCH 3/5] refactor: Allow run-test-case without tes, running all test from group --- bin/run-test-case | 102 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 31 deletions(-) diff --git a/bin/run-test-case b/bin/run-test-case index ff2e867b..4575caa6 100755 --- a/bin/run-test-case +++ b/bin/run-test-case @@ -36,24 +36,25 @@ foreach ($argv as $arg) { if (count($arArgs) < 2 || isset($arOptions['--help']) || isset($arOptions['-h'])) { echo << "/" +Run JSON Schema test suite test cases +Usage: run-test-case "[/]" Arguments: test-file Path to a test JSON file. Resolved in order: 1. As given (absolute or relative to CWD) 2. Relative to the bundled test suite root - description "/" — split on the first "/" only. + description "" or "/" — split on the first "/" only. Exact match is tried first, then case-insensitive substring. + Omit the test part to run all tests in the group. Options: -v --verbose Also print the schema -h --help Show this help Exit codes: - 0 Test found and result matches expected - 1 Test found but result does not match expected - 2 Usage error, file not found, or test not found + 0 All matched tests passed + 1 At least one test failed + 2 Usage error, file not found, or group/test not found HLP; exit(0); @@ -148,17 +149,6 @@ if ($testDesc !== null && $foundTest === null) { exit(2); } -// No test description given — list tests in the matched group -if ($foundTest === null) { - echo "Group: {$foundGroup->description}\n"; - echo "Tests:\n"; - foreach ($foundGroup->tests as $test) { - $expect = $test->valid ? 'valid' : 'invalid'; - echo " [{$expect}] {$test->description}\n"; - } - exit(0); -} - // Determine check mode (mirrors JsonSchemaTestSuiteTest::getCheckModeForDraft) $checkMode = in_array($draft, ['draft6', 'draft7'], true) ? Constraint::CHECK_MODE_NORMAL | Constraint::CHECK_MODE_STRICT @@ -192,36 +182,86 @@ if (is_dir($remotesDir)) { $factory = new Factory($schemaStorage); $factory->setDefaultDialect($draftIdentifier->getValue()); -$validator = new Validator($factory); -try { - $validator->validate($foundTest->data, $schema, $checkMode); -} catch (\Exception $e) { - echo "Error during validation: " . $e->getMessage() . "\n"; - exit(2); +/** @param object $test */ +$runTest = static function (object $test) use ($factory, $schema, $checkMode): array { + $validator = new Validator($factory); + try { + $validator->validate($test->data, $schema, $checkMode); + } catch (\Exception $e) { + return ['error' => $e->getMessage()]; + } + $isValid = count($validator->getErrors()) === 0; + return [ + 'passed' => $isValid === $test->valid, + 'isValid' => $isValid, + 'errors' => $validator->getErrors(), + ]; +}; + +$verbose = isset($arOptions['--verbose']) || isset($arOptions['-v']); + +// Run all tests in the group when no specific test was requested +if ($foundTest === null) { + echo "Group: {$foundGroup->description}\n"; + if ($verbose) { + echo "Schema: " . json_encode($schema, JSON_PRETTY_PRINT) . "\n"; + } + $passCount = 0; + $failCount = 0; + foreach ($foundGroup->tests as $test) { + $result = $runTest($test); + if (isset($result['error'])) { + echo sprintf("[ERROR] %s\n", $test->description); + echo " Error during validation: {$result['error']}\n"; + $failCount++; + continue; + } + $expect = $test->valid ? 'valid' : 'invalid'; + $label = $result['passed'] ? 'PASS' : 'FAIL'; + echo sprintf("[%s] [%s] %s\n", $label, $expect, $test->description); + if (!$result['passed']) { + if ($result['isValid']) { + echo " Validator returned valid but the test case expects invalid.\n"; + } else { + foreach ($result['errors'] as $error) { + echo sprintf(" [%s] %s\n", $error['property'], $error['message']); + } + } + $failCount++; + } else { + $passCount++; + } + } + echo "\n{$passCount} passed, {$failCount} failed.\n"; + exit($failCount > 0 ? 1 : 0); } -$isValid = count($validator->getErrors()) === 0; -$passed = $isValid === $foundTest->valid; +$result = $runTest($foundTest); + +if (isset($result['error'])) { + echo "Error during validation: {$result['error']}\n"; + exit(2); +} echo "Group : {$foundGroup->description}\n"; echo "Test : {$foundTest->description}\n"; -if (isset($arOptions['--verbose']) || isset($arOptions['-v'])) { +if ($verbose) { echo "Schema: " . json_encode($schema, JSON_PRETTY_PRINT) . "\n"; } echo "Data : " . json_encode($foundTest->data) . "\n"; echo "Expect: " . ($foundTest->valid ? 'valid' : 'invalid') . "\n"; -echo "Result: " . ($passed ? 'PASS' : 'FAIL') . "\n"; +echo "Result: " . ($result['passed'] ? 'PASS' : 'FAIL') . "\n"; -if (!$passed) { - if ($isValid) { +if (!$result['passed']) { + if ($result['isValid']) { echo "\nValidator returned valid but the test case expects invalid.\n"; } else { echo "\nValidation errors:\n"; - foreach ($validator->getErrors() as $error) { + foreach ($result['errors'] as $error) { echo sprintf(" [%s] %s\n", $error['property'], $error['message']); } } } -exit($passed ? 0 : 1); +exit($result['passed'] ? 0 : 1); From 13459c651aa78bab83a4724be3c534a7b8386a89 Mon Sep 17 00:00:00 2001 From: Danny van der Sluijs Date: Thu, 4 Jun 2026 20:59:56 +0200 Subject: [PATCH 4/5] test: Skip more optional failling tests --- tests/JsonSchemaTestSuiteTest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/JsonSchemaTestSuiteTest.php b/tests/JsonSchemaTestSuiteTest.php index 68132ae4..aec88cd6 100644 --- a/tests/JsonSchemaTestSuiteTest.php +++ b/tests/JsonSchemaTestSuiteTest.php @@ -181,8 +181,17 @@ private function shouldNotYieldTest(string $name): bool // Optional: bignum — PHP does not natively support arbitrary-precision integers/floats '[draft3/optional/bignum.json]: integer: a bignum is an integer is expected to be valid', '[draft3/optional/bignum.json]: integer: a negative bignum is an integer is expected to be valid', + '[draft3/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', '[draft4/optional/bignum.json]: integer: a bignum is an integer is expected to be valid', + '[draft4/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', + '[draft3/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', '[draft4/optional/bignum.json]: integer: a negative bignum is an integer is expected to be valid', + '[draft4/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', + '[draft4/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', + '[draft6/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', + '[draft6/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', + '[draft7/optional/bignum.json]: float comparison with high precision: comparison works for high numbers is expected to be invalid', + '[draft7/optional/bignum.json]: float comparison with high precision on negative numbers: comparison works for very negative numbers is expected to be invalid', // Optional: float-overflow — PHP float precision differs from the ECMAScript model '[draft4/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', '[draft6/optional/float-overflow.json]: all integers are multiples of 0.5, if overflow is handled: valid if optional overflow handling is implemented is expected to be valid', From 2f897d1a92116688cbd93bc8a29c7b8ced5e9f33 Mon Sep 17 00:00:00 2001 From: Danny van der Sluijs Date: Fri, 5 Jun 2026 15:14:27 +0200 Subject: [PATCH 5/5] fix: Correct for zulu when used with PHP > 8.0 --- .../Constraints/Drafts/Draft07/FormatConstraint.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php b/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php index ade50215..30f6c1c8 100644 --- a/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php +++ b/src/JsonSchema/Constraints/Drafts/Draft07/FormatConstraint.php @@ -136,9 +136,14 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n private function validateDateTime(string $datetime, string $format): bool { $datetime = strtoupper($datetime); // Cleanup for lowercase z + $isPhpLt80WithZulu = PHP_VERSION_ID < 80000 && substr($datetime, -1) === 'Z'; $isLeap = substr($datetime, 6, 2) === '60'; $input = $datetime; + // Correct for Zulu in PHP < 8.0 + if ($isPhpLt80WithZulu) { + $input = sprintf('%s+00:00', substr($input, 0, -1)); + } // Correct for leap second if ($isLeap) { $input = sprintf('%s59%s', substr($datetime, 0, 6), substr($datetime, 8)); @@ -157,11 +162,11 @@ private function validateDateTime(string $datetime, string $format): bool $expected = $dt->format($format); // Correct for trailing zeros on microseconds - if ($format === 'H:i:s.up') { + if ($format === 'H:i:s.up' || $format === 'H:i:s.uP') { $expected = sprintf( '%s%s', rtrim($dt->format('H:i:s.u'), '0'), - $dt->format('p') + $dt->format(substr($format, -1)) ); } // Correct back for leap seconds @@ -174,6 +179,10 @@ private function validateDateTime(string $datetime, string $format): bool $expected = sprintf('%s60%s', substr($expected, 0, 6), substr($expected, 8)); } + // Correct back for PHP > 8.0 and Zulu + if ($isPhpLt80WithZulu) { + $expected = sprintf('%sZ', substr($expected, 0, -6)); + } return $datetime === $expected; }