Skip to content

feat: add proof-of-concept implementation for strict fully validating using draft-06 schema #835

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 21 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
90a858e
feat: add proof-of-concept implementation for strictfully validating …
DannyvdSluijs Jun 25, 2025
6ef38b3
feat: more progress on draft-06 proof of concept
DannyvdSluijs Jun 25, 2025
022342a
feat: more support on draft-06 schema
DannyvdSluijs Jun 26, 2025
d2c074d
feat: Fixing multiple of; Removing dedicated draft-06 test case
DannyvdSluijs Jun 27, 2025
ff00ecc
fix: restore number constraint for draft-03 and draft-04
DannyvdSluijs Jun 27, 2025
3e7a8c4
feat: add more support for draft06 schema
DannyvdSluijs Jun 27, 2025
e1a6b90
feat: add more support for draft 06
DannyvdSluijs Jun 27, 2025
e09fed3
feat: add more draft-06 support
DannyvdSluijs Jun 27, 2025
29b5eaf
feat: more support for draft-06
DannyvdSluijs Jun 27, 2025
a319158
style: correct code style violations
DannyvdSluijs Jun 27, 2025
376709c
refactor: resolve phpstan found issues
DannyvdSluijs Jun 27, 2025
551a190
feat: more support for draft-06
DannyvdSluijs Jun 27, 2025
605fbec
fix: resolve dependencies keyword for oject with subschema
DannyvdSluijs Jun 30, 2025
80638e7
feat: add support for $ref
DannyvdSluijs Jun 30, 2025
2f6cecc
fix: Fix multiple off implementation
DannyvdSluijs Jun 30, 2025
1861371
fix: include $id in schema resolving as draft-06 and upwards use $id …
DannyvdSluijs Jul 1, 2025
80337e3
fix: fixes for unique items, pattern properties and additional proper…
DannyvdSluijs Jul 1, 2025
edcdde2
fix: fix for minimum keyword
DannyvdSluijs Jul 1, 2025
7b9ea0e
feat: Improving on format keyword
DannyvdSluijs Jul 4, 2025
286da5b
fix: Resolve failling test cases for format hostname
DannyvdSluijs Jul 4, 2025
4f24aca
fix: add handling of uri-template format; fail on backslash in uri re…
DannyvdSluijs Jul 4, 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
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,72 @@
<?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("/{$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]);
}
}
}
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());
}
}
}
47 changes: 47 additions & 0 deletions src/JsonSchema/Constraints/Drafts/Draft06/AnyOfConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?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()) {
return;
}
} catch (ValidationException $e) {
}
}

$this->addError(ConstraintError::ANY_OF(), $path);
}
}
35 changes: 35 additions & 0 deletions src/JsonSchema/Constraints/Drafts/Draft06/ConstConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace JsonSchema\Constraints\Drafts\Draft06;

use JsonSchema\ConstraintError;
use JsonSchema\Constraints\ConstraintInterface;
use JsonSchema\Constraints\Factory;
use JsonSchema\Entity\ErrorBagProxy;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Tool\DeepComparer;

class ConstConstraint implements ConstraintInterface
{
use ErrorBagProxy;

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

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

if (DeepComparer::isEqual($value, $schema->const)) {
return;
}

$this->addError(ConstraintError::CONSTANT(), $path, ['const' => $schema->const]);
}
}
47 changes: 47 additions & 0 deletions src/JsonSchema/Constraints/Drafts/Draft06/ContainsConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?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 ContainsConstraint 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, 'contains')) {
return;
}

$properties = [];
if (!is_array($value)) {
return;
}

foreach ($value as $propertyName => $propertyValue) {
$schemaConstraint = $this->factory->createInstanceFor('schema');

$schemaConstraint->check($propertyValue, $schema->contains, $path, $i);
if ($schemaConstraint->isValid()) {
return;
}
}

$this->addError(ConstraintError::CONTAINS(), $path, ['contains' => $schema->contains]);
}
}
Loading
Loading