Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e0da935
feat: add proof-of-concept implementation for strictfully validating …
DannyvdSluijs Jun 25, 2025
c66738b
feat: more progress on draft-06 proof of concept
DannyvdSluijs Jun 25, 2025
78df525
feat: more support on draft-06 schema
DannyvdSluijs Jun 26, 2025
744e94c
feat: Fixing multiple of; Removing dedicated draft-06 test case
DannyvdSluijs Jun 27, 2025
7971873
fix: restore number constraint for draft-03 and draft-04
DannyvdSluijs Jun 27, 2025
0c25bff
feat: add more support for draft06 schema
DannyvdSluijs Jun 27, 2025
c4da979
feat: add more support for draft 06
DannyvdSluijs Jun 27, 2025
48554c3
feat: add more draft-06 support
DannyvdSluijs Jun 27, 2025
a1d7f38
feat: more support for draft-06
DannyvdSluijs Jun 27, 2025
24cc51e
style: correct code style violations
DannyvdSluijs Jun 27, 2025
49d9fca
refactor: resolve phpstan found issues
DannyvdSluijs Jun 27, 2025
295d832
feat: more support for draft-06
DannyvdSluijs Jun 27, 2025
ae531cb
fix: resolve dependencies keyword for oject with subschema
DannyvdSluijs Jun 30, 2025
585ed7a
feat: add support for $ref
DannyvdSluijs Jun 30, 2025
c0165c4
fix: Fix multiple off implementation
DannyvdSluijs Jun 30, 2025
bdc29e2
fix: include $id in schema resolving as draft-06 and upwards use $id …
DannyvdSluijs Jul 1, 2025
7a7c94b
fix: fixes for unique items, pattern properties and additional proper…
DannyvdSluijs Jul 1, 2025
3592eeb
fix: fix for minimum keyword
DannyvdSluijs Jul 1, 2025
f11266d
feat: Improving on format keyword
DannyvdSluijs Jul 4, 2025
6182103
fix: Resolve failling test cases for format hostname
DannyvdSluijs Jul 4, 2025
fe502a1
fix: add handling of uri-template format; fail on backslash in uri re…
DannyvdSluijs Jul 4, 2025
365ef10
style: correct code style violations
DannyvdSluijs Jul 8, 2025
c57ec09
fix: fix optional regex testcase for ecmascript support
DannyvdSluijs Jul 8, 2025
c5c1cc6
refactor: Correct type hints in MultipleOf
DannyvdSluijs Jul 9, 2025
b18a276
refactor: correct more PHPStan discoverd issues
DannyvdSluijs Jul 9, 2025
58fff87
fix: fix edge cases with $ref
DannyvdSluijs Jul 9, 2025
3b283a9
refactor: improve any of error messages
DannyvdSluijs Jul 9, 2025
b5e9d58
test: Skip same test for draft 6
DannyvdSluijs Jul 22, 2025
efdbe08
style: Correct code style violations
DannyvdSluijs Jul 22, 2025
6a8d038
refactor: resolve PHPStan errors
DannyvdSluijs Jul 22, 2025
19c8b42
fix: resolve phpstan errors
DannyvdSluijs Jul 23, 2025
5d4b239
style: correct code style violations
DannyvdSluijs Jul 23, 2025
05b2bae
test: Block failling test which where blocked in previous drafts as well
DannyvdSluijs Aug 22, 2025
9ee6f5f
refactor: Resolce PHPStan issues in MultipleOfConstraint
DannyvdSluijs Aug 22, 2025
fba8900
refactor: Resolve PHPStan issues from Rfc3339
DannyvdSluijs Aug 22, 2025
eed0a52
style: Correct code style violations
DannyvdSluijs Aug 22, 2025
4b04863
refactor: Resolve hadrcoded draft6 solutions
DannyvdSluijs Aug 22, 2025
ca4da9b
Allow for time-numoffset with and without colon separator
matason Aug 23, 2025
4fabfe8
refactor: Introduce DraftIdentifers enum
DannyvdSluijs Aug 29, 2025
74aa902
test: Skip remaining failing testcases
DannyvdSluijs Aug 29, 2025
2c6657f
style: Correct code style violations
DannyvdSluijs Aug 29, 2025
3730e0a
style: Correct PHPStan found errors
DannyvdSluijs Aug 29, 2025
6537cca
fix: Add draft 6 remapping to json schema draft on disk
DannyvdSluijs Aug 29, 2025
4490b03
docs: Add clarification on default dialect
DannyvdSluijs Sep 19, 2025
4cdf05e
refactor: Remove inserting draft version as validator can handle miss…
DannyvdSluijs Sep 19, 2025
d6cd885
style: Correct code style violations
DannyvdSluijs Sep 19, 2025
8ed2a6f
test: Add test case for unsupported drafts in strict mode
DannyvdSluijs Sep 19, 2025
5068d0f
docs: Add changelog entry
DannyvdSluijs Oct 10, 2025
ecd4fb0
docs: Extend readme with info about strict mode
DannyvdSluijs Oct 10, 2025
b02b487
docs: Include bowtie report badges
DannyvdSluijs Oct 10, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Add lint check for class autoloading PSR compliance ([#845](https://github.com/jsonrainbow/json-schema/pull/845))
- add implementation for strict fully validating using draft-06 schema ([#843](https://github.com/jsonrainbow/json-schema/pull/835))

## [6.5.2] - 2025-09-09
### Fixed
Expand Down
44 changes: 27 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@
[![Build Status](https://github.com/jsonrainbow/json-schema/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/jsonrainbow/json-schema/actions)
[![Latest Stable Version](https://poser.pugx.org/justinrainbow/json-schema/v/stable)](https://packagist.org/packages/justinrainbow/json-schema)
[![Total Downloads](https://poser.pugx.org/justinrainbow/json-schema/downloads)](https://packagist.org/packages/justinrainbow/json-schema/stats)
![Supported Dialects](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fphp-justinrainbow-json-schema%2Fsupported_versions.json)

A PHP Implementation for validating `JSON` Structures against a given `Schema` with support for `Schemas` of Draft-3 or Draft-4. Features of newer Drafts might not be supported. See [Table of All Versions of Everything](https://json-schema.org/specification-links.html#table-of-all-versions-of-everything) to get an overview of all existing Drafts.
A PHP Implementation for validating `JSON` Structures against a given `Schema` with support for `Schemas` of Draft-3,
Draft-4 or Draft-6.

Features of newer Drafts might not be supported. See [Table of All Versions of Everything](https://json-schema.org/specification-links.html#table-of-all-versions-of-everything) to get an overview
of all existing Drafts. See [json-schema](http://json-schema.org/) for more details about the JSON Schema specification

# Compliance
![Draft 3](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fphp-justinrainbow-json-schema%2Fcompliance%2Fdraft3.json)
![Draft 4](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fphp-justinrainbow-json-schema%2Fcompliance%2Fdraft4.json)
![Draft 6](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fphp-justinrainbow-json-schema%2Fcompliance%2Fdraft6.json)

See [json-schema](http://json-schema.org/) for more details.

## Installation

Expand Down Expand Up @@ -187,24 +196,25 @@ A number of flags are available to alter the behavior of the validator. These ca
third argument to `Validator::validate()`, or can be provided as the third argument to
`Factory::__construct()` if you wish to persist them across multiple `validate()` calls.

| Flag | Description |
|------|-------------|
| `Constraint::CHECK_MODE_NORMAL` | Validate in 'normal' mode - this is the default |
| `Constraint::CHECK_MODE_TYPE_CAST` | Enable fuzzy type checking for associative arrays and objects |
| `Constraint::CHECK_MODE_COERCE_TYPES` | Convert data types to match the schema where possible |
| `Constraint::CHECK_MODE_EARLY_COERCE` | Apply type coercion as soon as possible |
| `Constraint::CHECK_MODE_APPLY_DEFAULTS` | Apply default values from the schema if not set |
| `Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS` | When applying defaults, only set values that are required |
| `Constraint::CHECK_MODE_EXCEPTIONS` | Throw an exception immediately if validation fails |
| `Constraint::CHECK_MODE_DISABLE_FORMAT` | Do not validate "format" constraints |
| `Constraint::CHECK_MODE_VALIDATE_SCHEMA` | Validate the schema as well as the provided document |

Please note that using `CHECK_MODE_COERCE_TYPES` or `CHECK_MODE_APPLY_DEFAULTS` will modify your
| Flag | Description |
|-------------------------------------------------|-----------------------------------------------------------------|
| `Constraint::CHECK_MODE_NORMAL` | Validate in 'normal' mode - this is the default |
| `Constraint::CHECK_MODE_TYPE_CAST` | Enable fuzzy type checking for associative arrays and objects |
| `Constraint::CHECK_MODE_COERCE_TYPES` [^1][^2] | Convert data types to match the schema where possible |
| `Constraint::CHECK_MODE_EARLY_COERCE` [^2] | Apply type coercion as soon as possible |
| `Constraint::CHECK_MODE_APPLY_DEFAULTS` [^1] | Apply default values from the schema if not set |
| `Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS` | When applying defaults, only set values that are required |
| `Constraint::CHECK_MODE_EXCEPTIONS` | Throw an exception immediately if validation fails |
| `Constraint::CHECK_MODE_DISABLE_FORMAT` | Do not validate "format" constraints |
| `Constraint::CHECK_MODE_VALIDATE_SCHEMA` | Validate the schema as well as the provided document |
| `Constraint::CHECK_MODE_STRICT` [^3] | Validate the scheme using strict mode using the specified draft |

[^1]: Please note that using `CHECK_MODE_COERCE_TYPES` or `CHECK_MODE_APPLY_DEFAULTS` will modify your
original data.

`CHECK_MODE_EARLY_COERCE` has no effect unless used in combination with `CHECK_MODE_COERCE_TYPES`. If
[^2]: `CHECK_MODE_EARLY_COERCE` has no effect unless used in combination with `CHECK_MODE_COERCE_TYPES`. If
enabled, the validator will use (and coerce) the first compatible type it encounters, even if the
schema defines another type that matches directly and does not require coercion.
[^3]: `CHECK_MODE_STRICT` only can be used for Draft-6 at this point.

## Running the tests

Expand Down
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());
}
}
}
Loading