Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e5aa59f
Update to json schema test suite main branch (#906)
DannyvdSluijs May 5, 2026
5847fcc
Display the additional property on error instead of the value (#904)
nicosp May 5, 2026
42ba3cc
feat: Add draft2019 as copy from draft07
DannyvdSluijs Feb 15, 2026
61247d0
fix: Fixes for minContains and maxContains
DannyvdSluijs Feb 18, 2026
04b132a
fix: Skip validation of content due to security concerns
DannyvdSluijs Feb 18, 2026
55f7830
feat: Add dependentSchemas support
DannyvdSluijs Mar 24, 2026
5edc502
feat: Add support for dependentRequired
DannyvdSluijs Mar 24, 2026
9691dca
fix: Add cont and enum support to propertyNames constraint
DannyvdSluijs May 4, 2026
16567e4
test: Skip recursiveRef.json test for draft 2019-09
DannyvdSluijs May 4, 2026
60fbc73
refactor: Correct return value scope
DannyvdSluijs May 4, 2026
d99f002
style: Remove unused imports
DannyvdSluijs May 4, 2026
abd861d
test: Skip more test for draft 2019-09
DannyvdSluijs May 4, 2026
fbe53a1
test: Skip more test fomr initial implementation
DannyvdSluijs May 13, 2026
7268a6b
fix: Correct PHPStan finding
DannyvdSluijs May 13, 2026
92a2a81
refactor: Address review comments and fix accross other drafts
DannyvdSluijs May 17, 2026
ef9b287
refactor: Process feedback
DannyvdSluijs Jun 8, 2026
25c175e
fix: Include json-schema-draft-2019-09.json schema
DannyvdSluijs Jun 11, 2026
75fb83b
docs: Update docs to reflect Draft 2019-09 support
DannyvdSluijs Jun 16, 2026
7a4e9f7
fix: Correct path for constraints that loop over properties
DannyvdSluijs Jun 16, 2026
c60cbf4
refactor: Remove redundant check
DannyvdSluijs Jun 16, 2026
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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
![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,
Draft-4, Draft-6 or Draft-7.
Draft-4, Draft-6,Draft-7 or Draft 2019-09.

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
Expand All @@ -15,7 +15,8 @@ of all existing Drafts. See [json-schema](http://json-schema.org/) for more deta
![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)
![Draft 7](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fphp-justinrainbow-json-schema%2Fcompliance%2Fdraft7.json)
![Draft 7](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fphp-justinrainbow-json-schema%2Fcompliance%2Fdraft7.json)))
![Draft 2019-09](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fphp-justinrainbow-json-schema%2Fdraft2019-09.json)


## Installation
Expand Down Expand Up @@ -215,7 +216,7 @@ original data.
[^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.
[^3]: `CHECK_MODE_STRICT` only can be used for Draft-6, Draft 7 or Draft 2019-09 at this point.

## Running the tests

Expand Down
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@
},
"require-dev": {
"friendsofphp/php-cs-fixer": "3.3.0",
"json-schema/json-schema-test-suite": "^23.2",
"phpunit/phpunit": "^8.5",
"phpspec/prophecy": "^1.19",
"phpstan/phpstan": "^1.12",
"marc-mabe/php-enum-phpstan": "^2.0"
"marc-mabe/php-enum-phpstan": "^2.0",
"json-schema/json-schema-test-suite": "dev-main"
},
"extra": {
"branch-alias": {
Expand All @@ -59,11 +59,11 @@
"type": "package",
"package": {
"name": "json-schema/json-schema-test-suite",
"version": "23.2.0",
"version": "dev-main",
"source": {
"type": "git",
"url": "https://github.com/json-schema/JSON-Schema-Test-Suite",
"reference": "23.2.0"
"reference": "main"
}
Comment thread
DannyvdSluijs marked this conversation as resolved.
}
}
Expand Down
152 changes: 152 additions & 0 deletions dist/schema/json-schema-draft-2019-09.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://json-schema.org/draft/2019-09/schema",
"title": "Core and Validation specifications meta-schema",
"type": ["object", "boolean"],
"$defs": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"allOf": [
{ "$ref": "#/$defs/nonNegativeInteger" },
{ "default": 0 }
]
},
"simpleTypes": {
"enum": ["array", "boolean", "integer", "null", "number", "object", "string"]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
},
"properties": {
"$id": { "type": "string", "format": "uri-reference" },
"$schema": { "type": "string", "format": "uri" },
"$anchor": { "type": "string", "pattern": "^[A-Za-z][-A-Za-z0-9.:_]*$" },
"$ref": { "type": "string", "format": "uri-reference" },
"$recursiveRef": { "type": "string", "format": "uri-reference" },
"$recursiveAnchor": { "type": "boolean", "default": false },
"$vocabulary": {
"type": "object",
"propertyNames": { "type": "string", "format": "uri" },
"additionalProperties": { "type": "boolean" }
},
"$comment": { "type": "string" },
"$defs": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"definitions": {
"$comment": "Retained for backwards compatibility, replaced by $defs.",
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"title": { "type": "string" },
"description": { "type": "string" },
"default": true,
"deprecated": { "type": "boolean", "default": false },
"readOnly": { "type": "boolean", "default": false },
"writeOnly": { "type": "boolean", "default": false },
"examples": { "type": "array", "items": true },
"multipleOf": { "type": "number", "exclusiveMinimum": 0 },
"maximum": { "type": "number" },
"exclusiveMaximum": { "type": "number" },
"minimum": { "type": "number" },
"exclusiveMinimum": { "type": "number" },
"maxLength": { "$ref": "#/$defs/nonNegativeInteger" },
"minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"pattern": { "type": "string", "format": "regex" },
"additionalItems": { "$ref": "#" },
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/$defs/schemaArray" }
],
"default": true
},
"maxItems": { "$ref": "#/$defs/nonNegativeInteger" },
"minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"uniqueItems": { "type": "boolean", "default": false },
"contains": { "$ref": "#" },
"maxContains": { "$ref": "#/$defs/nonNegativeInteger" },
"minContains": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"unevaluatedItems": { "$ref": "#" },
"maxProperties": { "$ref": "#/$defs/nonNegativeInteger" },
"minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" },
"required": { "$ref": "#/$defs/stringArray" },
"additionalProperties": { "$ref": "#" },
"unevaluatedProperties": { "$ref": "#" },
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"propertyNames": { "format": "regex" },
"default": {}
},
"dependentSchemas": {
"type": "object",
"additionalProperties": { "$ref": "#" }
},
"dependentRequired": {
"type": "object",
"additionalProperties": { "$ref": "#/$defs/stringArray" }
},
"dependencies": {
"$comment": "Retained for backwards compatibility, replaced by dependentSchemas/dependentRequired.",
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/$defs/stringArray" }
]
}
},
"propertyNames": { "$ref": "#" },
"const": true,
"enum": {
"type": "array",
"items": true,
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/$defs/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/$defs/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"format": { "type": "string" },
"contentEncoding": { "type": "string" },
"contentMediaType": { "type": "string" },
"contentSchema": { "$ref": "#" },
"if": { "$ref": "#" },
"then": { "$ref": "#" },
"else": { "$ref": "#" },
"allOf": { "$ref": "#/$defs/schemaArray" },
"anyOf": { "$ref": "#/$defs/schemaArray" },
"oneOf": { "$ref": "#/$defs/schemaArray" },
"not": { "$ref": "#" }
},
"default": true
}
4 changes: 4 additions & 0 deletions src/JsonSchema/ConstraintError.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ class ConstraintError extends Enum
public const INVALID_SCHEMA = 'invalidSchema';
public const LENGTH_MAX = 'maxLength';
public const LENGTH_MIN = 'minLength';
public const MAX_CONTAINS = 'maxContains';
public const MAXIMUM = 'maximum';
public const MIN_CONTAINS = 'minContains';
public const MIN_ITEMS = 'minItems';
public const MINIMUM = 'minimum';
public const MISSING_ERROR = 'missingError';
Expand Down Expand Up @@ -99,8 +101,10 @@ public function getMessage()
self::LENGTH_MAX => 'Must be at most %d characters long',
self::INVALID_SCHEMA => 'Schema is not valid',
self::LENGTH_MIN => 'Must be at least %d characters long',
self::MAX_CONTAINS => 'There must be a maximum of %d valid items in the array, %d found',
self::MAX_ITEMS => 'There must be a maximum of %d items in the array, %d found',
self::MAXIMUM => 'Must have a maximum value less than or equal to %d',
self::MIN_CONTAINS => 'There must be a minimum of %d valid items in the array, %d found',
self::MIN_ITEMS => 'There must be a minimum of %d items in the array, %d found',
self::MINIMUM => 'Must have a minimum value greater than or equal to %d',
self::MISSING_MAXIMUM => 'Use of exclusiveMaximum requires presence of maximum',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ private function validateColor(string $color): bool
return true;
}

return preg_match('/^#([a-f0-9]{3}|[a-f0-9]{6})$/i', $color) !== false;
return preg_match('/^#([a-f0-9]{3}|[a-f0-9]{6})$/i', $color) === 1;
}

private function validateStyle(string $style): bool
Expand All @@ -168,7 +168,7 @@ private function validateStyle(string $style): bool

private function validatePhone(string $phone): bool
{
return preg_match('/^\+?(\(\d{3}\)|\d{3}) \d{3} \d{4}$/', $phone) !== false;
return preg_match('/^\+?(\(\d{3}\)|\d{3}) \d{3} \d{4}$/', $phone) === 1;
}

private function validateHostname(string $host): bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,20 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
}
}
}

if (property_exists($schema->propertyNames, 'const')) {
foreach ($propertyNames as $propertyName => $_) {
if ($propertyName !== $schema->propertyNames->const) {
$this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'const', 'name' => $propertyName]);
}
}
}

if (property_exists($schema->propertyNames, 'enum')) {
$diff = array_diff(array_keys($propertyNames), $schema->propertyNames->enum);
foreach ($diff as $propertyName) {
$this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'enum', 'name' => $propertyName]);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ private function validateColor(string $color): bool
return true;
}

return preg_match('/^#([a-f0-9]{3}|[a-f0-9]{6})$/i', $color) !== false;
return preg_match('/^#([a-f0-9]{3}|[a-f0-9]{6})$/i', $color) === 1;
}

private function validateStyle(string $style): bool
Expand All @@ -221,7 +221,7 @@ private function validateStyle(string $style): bool

private function validatePhone(string $phone): bool
{
return preg_match('/^\+?(\(\d{3}\)|\d{3}) \d{3} \d{4}$/', $phone) !== false;
return preg_match('/^\+?(\(\d{3}\)|\d{3}) \d{3} \d{4}$/', $phone) === 1;
}

private function validateHostname(string $host): bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,20 @@ public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = n
}
}
}

if (property_exists($schema->propertyNames, 'const')) {
foreach ($propertyNames as $propertyName => $_) {
if ($propertyName !== $schema->propertyNames->const) {
$this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'const', 'name' => $propertyName]);
}
}
}

if (property_exists($schema->propertyNames, 'enum')) {
$diff = array_diff(array_keys($propertyNames), $schema->propertyNames->enum);
foreach ($diff as $propertyName) {
$this->addError(ConstraintError::PROPERTY_NAMES(), $path, ['propertyNames' => $schema->propertyNames, 'violating' => 'enum', 'name' => $propertyName]);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace JsonSchema\Constraints\Drafts\Draft2019;

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 (is_object($schema->items)) {
return;
}

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

foreach ($additionalItems as $propertyName => $propertyValue) {
$incrementedPath = $basePath->withPropertyPaths(array_merge($basePath->getPropertyPaths(), [$propertyName]));

$schemaConstraint = $this->factory->createInstanceFor('schema');
$schemaConstraint->check($propertyValue, $schema->additionalItems, $incrementedPath, $i);

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

$this->addError(ConstraintError::ADDITIONAL_ITEMS(), $incrementedPath, ['item' => $i, 'property' => $propertyName, 'additionalItems' => $schema->additionalItems]);
}
}
}
Loading
Loading