Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
3948340
feat: add proof-of-concept implementation for strictfully validating …
DannyvdSluijs Jun 25, 2025
6f45bb3
feat: more progress on draft-06 proof of concept
DannyvdSluijs Jun 25, 2025
92d117e
feat: more support on draft-06 schema
DannyvdSluijs Jun 26, 2025
2cdb35f
feat: Fixing multiple of; Removing dedicated draft-06 test case
DannyvdSluijs Jun 27, 2025
86c1ebd
fix: restore number constraint for draft-03 and draft-04
DannyvdSluijs Jun 27, 2025
4a21d75
feat: add more support for draft06 schema
DannyvdSluijs Jun 27, 2025
e97f2f1
feat: add more support for draft 06
DannyvdSluijs Jun 27, 2025
3b3db3a
feat: add more draft-06 support
DannyvdSluijs Jun 27, 2025
a4348c9
feat: more support for draft-06
DannyvdSluijs Jun 27, 2025
b430d9c
style: correct code style violations
DannyvdSluijs Jun 27, 2025
2ebdec9
refactor: resolve phpstan found issues
DannyvdSluijs Jun 27, 2025
09fa099
feat: more support for draft-06
DannyvdSluijs Jun 27, 2025
3565e29
fix: resolve dependencies keyword for oject with subschema
DannyvdSluijs Jun 30, 2025
fd82060
feat: add support for $ref
DannyvdSluijs Jun 30, 2025
ba27d2e
fix: Fix multiple off implementation
DannyvdSluijs Jun 30, 2025
cc63d8c
fix: include $id in schema resolving as draft-06 and upwards use $id …
DannyvdSluijs Jul 1, 2025
755c100
fix: fixes for unique items, pattern properties and additional proper…
DannyvdSluijs Jul 1, 2025
6bfeeb2
fix: fix for minimum keyword
DannyvdSluijs Jul 1, 2025
cc7f338
feat: Improving on format keyword
DannyvdSluijs Jul 4, 2025
528d97e
fix: Resolve failling test cases for format hostname
DannyvdSluijs Jul 4, 2025
221cb87
fix: add handling of uri-template format; fail on backslash in uri re…
DannyvdSluijs Jul 4, 2025
3c98180
style: correct code style violations
DannyvdSluijs Jul 8, 2025
6fd8399
fix: fix optional regex testcase for ecmascript support
DannyvdSluijs Jul 8, 2025
06e1b11
refactor: Correct type hints in MultipleOf
DannyvdSluijs Jul 9, 2025
4fc3b30
refactor: correct more PHPStan discoverd issues
DannyvdSluijs Jul 9, 2025
bf9bf22
fix: fix edge cases with $ref
DannyvdSluijs Jul 9, 2025
3e164de
refactor: improve any of error messages
DannyvdSluijs Jul 9, 2025
05d0eb0
test: Skip same test for draft 6
DannyvdSluijs Jul 22, 2025
6e3d3cc
style: Correct code style violations
DannyvdSluijs Jul 22, 2025
181d647
refactor: resolve PHPStan errors
DannyvdSluijs Jul 22, 2025
5023f95
fix: resolve phpstan errors
DannyvdSluijs Jul 23, 2025
4efe199
style: correct code style violations
DannyvdSluijs Jul 23, 2025
5a99b87
test: Block failling test which where blocked in previous drafts as well
DannyvdSluijs Aug 22, 2025
cc6e702
refactor: Resolce PHPStan issues in MultipleOfConstraint
DannyvdSluijs Aug 22, 2025
0f6df8b
refactor: Resolve PHPStan issues from Rfc3339
DannyvdSluijs Aug 22, 2025
32c6540
style: Correct code style violations
DannyvdSluijs Aug 22, 2025
8dff21a
refactor: Resolve hadrcoded draft6 solutions
DannyvdSluijs Aug 22, 2025
fc994a7
Allow for time-numoffset with and without colon separator
matason Aug 23, 2025
77156cd
refactor: Introduce DraftIdentifers enum
DannyvdSluijs Aug 29, 2025
90cbbe6
test: Skip remaining failing testcases
DannyvdSluijs Aug 29, 2025
2b489ea
style: Correct code style violations
DannyvdSluijs Aug 29, 2025
7b3c2c2
style: Correct PHPStan found errors
DannyvdSluijs Aug 29, 2025
94d2da8
fix: Add draft 6 remapping to json schema draft on disk
DannyvdSluijs Aug 29, 2025
73c84f6
fix: Ensure http_get_last_response_headers has clean state before mak…
DannyvdSluijs Sep 2, 2025
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
21 changes: 3 additions & 18 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,12 @@ parameters:
path: src/JsonSchema/Constraints/Factory.php

-
message: "#^Method JsonSchema\\\\Constraints\\\\Factory\\:\\:getConfig\\(\\) should return int\\<0, 511\\> but returns int\\.$#"
message: "#^Method JsonSchema\\\\Constraints\\\\Factory\\:\\:getConfig\\(\\) should return int\\<0, 1023\\> but returns int\\.$#"
count: 1
path: src/JsonSchema/Constraints/Factory.php

-
message: "#^Method JsonSchema\\\\Constraints\\\\Factory\\:\\:getErrorContext\\(\\) should return 1\\|2 but returns int\\.$#"
count: 1
path: src/JsonSchema/Constraints/Factory.php

-
message: "#^Property JsonSchema\\\\Constraints\\\\Factory\\:\\:\\$checkMode \\(int\\<0, 511\\>\\) does not accept int\\.$#"
message: "#^Property JsonSchema\\\\Constraints\\\\Factory\\:\\:\\$checkMode \\(int\\<0, 1023\\>\\) does not accept int\\.$#"
count: 2
path: src/JsonSchema/Constraints/Factory.php

Expand Down Expand Up @@ -630,16 +625,6 @@ parameters:
count: 1
path: src/JsonSchema/Iterator/ObjectIterator.php

-
message: "#^Offset 4 does not exist on array\\{0\\: string, 1\\: non\\-falsy\\-string, 2\\: string, 3\\: non\\-falsy\\-string, 4\\?\\: string, 5\\?\\: non\\-falsy\\-string&numeric\\-string\\}\\.$#"
count: 1
path: src/JsonSchema/Rfc3339.php

-
message: "#^Offset 5 does not exist on array\\{0\\: string, 1\\: non\\-falsy\\-string, 2\\: string, 3\\: non\\-falsy\\-string, 4\\?\\: string, 5\\?\\: non\\-falsy\\-string&numeric\\-string\\}\\.$#"
count: 1
path: src/JsonSchema/Rfc3339.php

-
message: "#^Access to an undefined property object\\:\\:\\$properties\\.$#"
count: 3
Expand All @@ -656,7 +641,7 @@ parameters:
path: src/JsonSchema/SchemaStorage.php

-
message: "#^Call to function is_array\\(\\) with object will always evaluate to false\\.$#"
message: "#^Call to function is_array\\(\\) with bool|object will always evaluate to false\\.$#"
count: 1
path: src/JsonSchema/SchemaStorage.php

Expand Down
10 changes: 10 additions & 0 deletions src/JsonSchema/ConstraintError.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,23 @@ class ConstraintError extends Enum
public const DIVISIBLE_BY = 'divisibleBy';
public const ENUM = 'enum';
public const CONSTANT = 'const';
public const CONTAINS = 'contains';
public const EXCLUSIVE_MINIMUM = 'exclusiveMinimum';
public const EXCLUSIVE_MAXIMUM = 'exclusiveMaximum';
public const FALSE = 'false';
public const FORMAT_COLOR = 'colorFormat';
public const FORMAT_DATE = 'dateFormat';
public const FORMAT_DATE_TIME = 'dateTimeFormat';
public const FORMAT_DATE_UTC = 'dateUtcFormat';
public const FORMAT_EMAIL = 'emailFormat';
public const FORMAT_HOSTNAME = 'styleHostName';
public const FORMAT_IP = 'ipFormat';
public const FORMAT_JSON_POINTER = 'jsonPointerFormat';
public const FORMAT_PHONE = 'phoneFormat';
public const FORMAT_REGEX= 'regexFormat';
public const FORMAT_STYLE = 'styleFormat';
public const FORMAT_TIME = 'timeFormat';
public const FORMAT_URI_TEMPLATE = 'uriTemplateFormat';
public const FORMAT_URL = 'urlFormat';
public const FORMAT_URL_REF = 'urlRefFormat';
public const INVALID_SCHEMA = 'invalidSchema';
Expand All @@ -51,6 +55,7 @@ class ConstraintError extends Enum
public const PREGEX_INVALID = 'pregrex';
public const PROPERTIES_MIN = 'minProperties';
public const PROPERTIES_MAX = 'maxProperties';
public const PROPERTY_NAMES = 'propertyNames';
public const TYPE = 'type';
public const UNIQUE_ITEMS = 'uniqueItems';

Expand All @@ -70,19 +75,23 @@ public function getMessage()
self::DIVISIBLE_BY => 'Is not divisible by %d',
self::ENUM => 'Does not have a value in the enumeration %s',
self::CONSTANT => 'Does not have a value equal to %s',
self::CONTAINS => 'Does not have a value valid to contains schema',
self::EXCLUSIVE_MINIMUM => 'Must have a minimum value greater than %d',
self::EXCLUSIVE_MAXIMUM => 'Must have a maximum value less than %d',
self::FALSE => 'Boolean schema false',
self::FORMAT_COLOR => 'Invalid color',
self::FORMAT_DATE => 'Invalid date %s, expected format YYYY-MM-DD',
self::FORMAT_DATE_TIME => 'Invalid date-time %s, expected format YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss+hh:mm',
self::FORMAT_DATE_UTC => 'Invalid time %s, expected integer of milliseconds since Epoch',
self::FORMAT_EMAIL => 'Invalid email',
self::FORMAT_HOSTNAME => 'Invalid hostname',
self::FORMAT_IP => 'Invalid IP address',
self::FORMAT_JSON_POINTER => 'Invalid JSON pointer',
self::FORMAT_PHONE => 'Invalid phone number',
self::FORMAT_REGEX=> 'Invalid regex format %s',
self::FORMAT_STYLE => 'Invalid style',
self::FORMAT_TIME => 'Invalid time %s, expected format hh:mm:ss',
self::FORMAT_URI_TEMPLATE => 'Invalid URI template format',
self::FORMAT_URL => 'Invalid URL format',
self::FORMAT_URL_REF => 'Invalid URL reference format',
self::LENGTH_MAX => 'Must be at most %d characters long',
Expand All @@ -104,6 +113,7 @@ public function getMessage()
self::PREGEX_INVALID => 'The pattern %s is invalid',
self::PROPERTIES_MIN => 'Must contain a minimum of %d properties',
self::PROPERTIES_MAX => 'Must contain no more than %d properties',
self::PROPERTY_NAMES => 'Property name %s is invalid',
self::TYPE => '%s value found, but %s is required',
self::UNIQUE_ITEMS => 'There are no duplicates allowed in the array'
];
Expand Down
1 change: 1 addition & 0 deletions src/JsonSchema/Constraints/Constraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ abstract class Constraint extends BaseConstraint implements ConstraintInterface
public const CHECK_MODE_EARLY_COERCE = 0x00000040;
public const CHECK_MODE_ONLY_REQUIRED_DEFAULTS = 0x00000080;
public const CHECK_MODE_VALIDATE_SCHEMA = 0x00000100;
public const CHECK_MODE_STRICT = 0x00000200;

/**
* Bubble down the path
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace JsonSchema\Constraints\Drafts\Draft06;

use JsonSchema\ConstraintError;
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;

class AdditionalItemsConstraint implements ConstraintInterface
{
use ErrorBagProxy;

/** @var Factory */
private $factory;

public function __construct(?Factory $factory = null)
{
$this->factory = $factory ?: new Factory();
$this->initialiseErrorBag($this->factory);
}

public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
if (!property_exists($schema, 'additionalItems')) {
return;
}

if ($schema->additionalItems === true) {
return;
}
if ($schema->additionalItems === false && !property_exists($schema, 'items')) {
return;
}

if (!is_array($value)) {
return;
}
if (!property_exists($schema, 'items')) {
return;
}
if (property_exists($schema, 'items') && is_object($schema->items)) {
return;
}

$additionalItems = array_diff_key($value, property_exists($schema, 'items') ? $schema->items : []);

foreach ($additionalItems as $propertyName => $propertyValue) {
$schemaConstraint = $this->factory->createInstanceFor('schema');
$schemaConstraint->check($propertyValue, $schema->additionalItems, $path, $i);

if ($schemaConstraint->isValid()) {
continue;
}

$this->addError(ConstraintError::ADDITIONAL_ITEMS(), $path, ['item' => $i, 'property' => $propertyName, 'additionalItems' => $schema->additionalItems]);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

declare(strict_types=1);

namespace JsonSchema\Constraints\Drafts\Draft06;

use JsonSchema\ConstraintError;
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;

class AdditionalPropertiesConstraint implements ConstraintInterface
{
use ErrorBagProxy;

/** @var Factory */
private $factory;

public function __construct(?Factory $factory = null)
{
$this->factory = $factory ?: new Factory();
$this->initialiseErrorBag($this->factory);
}

public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
if (!property_exists($schema, 'additionalProperties')) {
return;
}

if ($schema->additionalProperties === true) {
return;
}

if (!is_object($value)) {
return;
}

$additionalProperties = get_object_vars($value);

if (isset($schema->properties)) {
$additionalProperties = array_diff_key($additionalProperties, (array) $schema->properties);
}

if (isset($schema->patternProperties)) {
$patterns = array_keys(get_object_vars($schema->patternProperties));

foreach ($additionalProperties as $key => $_) {
foreach ($patterns as $pattern) {
if (preg_match($this->createPregMatchPattern($pattern), (string) $key)) {
unset($additionalProperties[$key]);
break;
}
}
}
}

if (is_object($schema->additionalProperties)) {
foreach ($additionalProperties as $key => $additionalPropertiesValue) {
$schemaConstraint = $this->factory->createInstanceFor('schema');
$schemaConstraint->check($additionalPropertiesValue, $schema->additionalProperties, $path, $i); // @todo increment path
if ($schemaConstraint->isValid()) {
unset($additionalProperties[$key]);
}
}
}

foreach ($additionalProperties as $key => $additionalPropertiesValue) {
$this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), $path, ['found' => $additionalPropertiesValue]);
}
}

private function createPregMatchPattern(string $pattern): string
{
$replacements = [
// '\D' => '[^0-9]',
// '\d' => '[0-9]',
'\p{digit}' => '\p{Nd}',
// '\w' => '[A-Za-z0-9_]',
// '\W' => '[^A-Za-z0-9_]',
// '\s' => '[\s\x{200B}]' // Explicitly include zero width white space,
'\p{Letter}' => '\p{L}', // Map ECMA long property name to PHP (PCRE) Unicode property abbreviations
];

$pattern = str_replace(
array_keys($replacements),
array_values($replacements),
$pattern
);

return '/' . str_replace('/', '\/', $pattern) . '/u';
}
}
42 changes: 42 additions & 0 deletions src/JsonSchema/Constraints/Drafts/Draft06/AllOfConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

declare(strict_types=1);

namespace JsonSchema\Constraints\Drafts\Draft06;

use JsonSchema\ConstraintError;
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;

class AllOfConstraint implements ConstraintInterface
{
use ErrorBagProxy;

/** @var Factory */
private $factory;

public function __construct(?Factory $factory = null)
{
$this->factory = $factory ?: new Factory();
$this->initialiseErrorBag($this->factory);
}

public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
if (!property_exists($schema, 'allOf')) {
return;
}

foreach ($schema->allOf as $allOfSchema) {
$schemaConstraint = $this->factory->createInstanceFor('schema');
$schemaConstraint->check($value, $allOfSchema, $path, $i);

if ($schemaConstraint->isValid()) {
continue;
}
$this->addError(ConstraintError::ALL_OF(), $path);
$this->addErrors($schemaConstraint->getErrors());
}
}
}
51 changes: 51 additions & 0 deletions src/JsonSchema/Constraints/Drafts/Draft06/AnyOfConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace JsonSchema\Constraints\Drafts\Draft06;

use JsonSchema\ConstraintError;
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Exception\ValidationException;

class AnyOfConstraint implements ConstraintInterface
{
use ErrorBagProxy;

/** @var Factory */
private $factory;

public function __construct(?Factory $factory = null)
{
$this->factory = $factory ?: new Factory();
$this->initialiseErrorBag($this->factory);
}

public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void
{
if (!property_exists($schema, 'anyOf')) {
return;
}

foreach ($schema->anyOf as $anyOfSchema) {
$schemaConstraint = $this->factory->createInstanceFor('schema');

try {
$schemaConstraint->check($value, $anyOfSchema, $path, $i);

if ($schemaConstraint->isValid()) {
$this->errorBag()->reset();

return;
}

$this->addErrors($schemaConstraint->getErrors());
} catch (ValidationException $e) {
}
}

$this->addError(ConstraintError::ANY_OF(), $path);
}
}
Loading