├── .php-cs-fixer.dist.php ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── UPGRADE-6.0.md ├── bin └── validate-json ├── composer.json ├── dist └── schema │ ├── json-schema-draft-03.json │ └── json-schema-draft-04.json └── src └── JsonSchema ├── ConstraintError.php ├── Constraints ├── BaseConstraint.php ├── CollectionConstraint.php ├── ConstConstraint.php ├── Constraint.php ├── ConstraintInterface.php ├── EnumConstraint.php ├── Factory.php ├── FormatConstraint.php ├── NumberConstraint.php ├── ObjectConstraint.php ├── SchemaConstraint.php ├── StringConstraint.php ├── TypeCheck │ ├── LooseTypeCheck.php │ ├── StrictTypeCheck.php │ └── TypeCheckInterface.php ├── TypeConstraint.php └── UndefinedConstraint.php ├── Entity └── JsonPointer.php ├── Enum.php ├── Exception ├── ExceptionInterface.php ├── InvalidArgumentException.php ├── InvalidConfigException.php ├── InvalidSchemaException.php ├── InvalidSchemaMediaTypeException.php ├── InvalidSourceUriException.php ├── JsonDecodingException.php ├── ResourceNotFoundException.php ├── RuntimeException.php ├── UnresolvableJsonPointerException.php ├── UriResolverException.php └── ValidationException.php ├── Iterator └── ObjectIterator.php ├── Rfc3339.php ├── SchemaStorage.php ├── SchemaStorageInterface.php ├── Tool ├── DeepComparer.php ├── DeepCopy.php └── Validator │ ├── RelativeReferenceValidator.php │ └── UriValidator.php ├── Uri ├── Retrievers │ ├── AbstractRetriever.php │ ├── Curl.php │ ├── FileGetContents.php │ ├── PredefinedArray.php │ └── UriRetrieverInterface.php ├── UriResolver.php └── UriRetriever.php ├── UriResolverInterface.php ├── UriRetrieverInterface.php └── Validator.php /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in([__DIR__ . '/src', __DIR__ . '/tests']); 6 | 7 | /* Based on ^2.1 of php-cs-fixer */ 8 | $config 9 | ->setRules([ 10 | // default 11 | '@PSR2' => true, 12 | '@Symfony' => true, 13 | // additionally 14 | 'array_syntax' => ['syntax' => 'short'], 15 | 'binary_operator_spaces' => false, 16 | 'concat_space' => ['spacing' => 'one'], 17 | 'increment_style' => false, 18 | 'no_superfluous_phpdoc_tags' => false, 19 | 'no_useless_else' => true, 20 | 'no_useless_return' => true, 21 | 'ordered_imports' => true, 22 | 'phpdoc_no_package' => false, 23 | 'phpdoc_order' => true, 24 | 'phpdoc_summary' => false, 25 | 'phpdoc_types_order' => ['null_adjustment' => 'none', 'sort_algorithm' => 'none'], 26 | 'simplified_null_return' => false, 27 | 'single_line_throw' => false, 28 | 'trailing_comma_in_multiline' => false, 29 | 'yoda_style' => false, 30 | ]) 31 | ->setFinder($finder) 32 | ; 33 | 34 | return $config; 35 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [6.4.2] - 2025-06-03 11 | ### Fixed 12 | - Fix objects are non-unique despite key order ([#819](https://github.com/jsonrainbow/json-schema/pull/819)) 13 | - Id's not being resolved and id property affects sibling ref which it should not do ([#828](https://github.com/jsonrainbow/json-schema/pull/828)) 14 | 15 | ### Changed 16 | - Added extra breaking change to UPDATE-6.0.md regarding BaseConstraint::addError signature change ([#823](https://github.com/jsonrainbow/json-schema/pull/823)) 17 | - Update constraint class to PHP 7.2 language level ([#824](https://github.com/jsonrainbow/json-schema/pull/824)) 18 | - Update base constraint class to PHP 7.2 language level ([#826](https://github.com/jsonrainbow/json-schema/pull/826)) 19 | 20 | ### Added 21 | - Introduce 32 bits CI workflow on latest php version ([#825](https://github.com/jsonrainbow/json-schema/pull/825)) 22 | 23 | ## [6.4.1] - 2025-04-04 24 | ### Fixed 25 | - Fix support for 32bits PHP ([#817](https://github.com/jsonrainbow/json-schema/pull/817)) 26 | 27 | ## [6.4.0] - 2025-04-01 28 | ### Added 29 | - Run PHPStan using the lowest and highest php version ([#811](https://github.com/jsonrainbow/json-schema/pull/811)) 30 | ### Fixed 31 | - Use parallel-lint and cs2pr for improved feedback on linting errors ([#812](https://github.com/jsonrainbow/json-schema/pull/812)) 32 | - Array with number values with mathematical equality are considered valid ([#813](https://github.com/jsonrainbow/json-schema/pull/813)) 33 | ### Changed 34 | - Correct PHPStan findings in validator ([#808](https://github.com/jsonrainbow/json-schema/pull/808)) 35 | - Add cs2pr handling for php-cs-fixer; avoid doing composer install ([#814](https://github.com/jsonrainbow/json-schema/pull/814)) 36 | - prepare PHP 8.5 in CI ([#815](https://github.com/jsonrainbow/json-schema/pull/815)) 37 | 38 | ## [6.3.1] - 2025-03-18 39 | ### Fixed 40 | - ensure numeric issues in const are correctly evaluated ([#805](https://github.com/jsonrainbow/json-schema/pull/805)) 41 | - fix 6.3.0 regression with comparison of null values during validation ([#806](https://github.com/jsonrainbow/json-schema/issues/806)) 42 | 43 | ## [6.3.0] - 2025-03-14 44 | ### Fixed 45 | - only check minProperties or maxProperties on objects ([#802](https://github.com/jsonrainbow/json-schema/pull/802)) 46 | - replace filter_var for uri and uri-reference to userland code to be RFC 3986 compliant ([#800](https://github.com/jsonrainbow/json-schema/pull/800)) 47 | - avoid duplicate workflow runs ([#804](https://github.com/jsonrainbow/json-schema/pull/804)) 48 | 49 | ## Changed 50 | - replace icecave/parity with custom deep comparator ([#803](https://github.com/jsonrainbow/json-schema/pull/803)) 51 | 52 | ## [6.2.1] - 2025-03-06 53 | ### Fixed 54 | - allow items: true to pass validation ([#801](https://github.com/jsonrainbow/json-schema/pull/801)) 55 | 56 | ### Changed 57 | - Include actual count in collection constraint errors ([#797](https://github.com/jsonrainbow/json-schema/pull/797)) 58 | 59 | ## [6.2.0] - 2025-02-26 60 | ### Added 61 | - Welcome first time contributors ([#782](https://github.com/jsonrainbow/json-schema/pull/782)) 62 | 63 | ### Fixed 64 | - Add required permissions for welcome action ([#789](https://github.com/jsonrainbow/json-schema/pull/789)) 65 | - Upgrade php cs fixer to latest ([#783](https://github.com/jsonrainbow/json-schema/pull/783)) 66 | - Create deep copy before checking each sub schema in oneOf ([#791](https://github.com/jsonrainbow/json-schema/pull/791)) 67 | - Create deep copy before checking each sub schema in anyOf ([#792](https://github.com/jsonrainbow/json-schema/pull/792)) 68 | - Correctly set the schema ID when passing it as assoc array ([#794](https://github.com/jsonrainbow/json-schema/pull/794)) 69 | - Create deep copy before checking each sub schema in oneOf when only check_mode_apply_defaults is set ([#795](https://github.com/jsonrainbow/json-schema/pull/795)) 70 | - Additional property casted into int when actually is numeric string ([#784](https://github.com/jsonrainbow/json-schema/pull/784)) 71 | 72 | ### Changed 73 | - Used PHPStan's int-mask-of type where applicable ([#779](https://github.com/jsonrainbow/json-schema/pull/779)) 74 | - Fixed some PHPStan errors ([#781](https://github.com/jsonrainbow/json-schema/pull/781)) 75 | - Cleanup redundant checks ([#796](https://github.com/jsonrainbow/json-schema/pull/796)) 76 | 77 | ## [6.1.0] - 2025-02-04 78 | ### Added 79 | - Add return types in the test suite ([#748](https://github.com/jsonrainbow/json-schema/pull/748)) 80 | - Add test case for validating array of strings with objects ([#704](https://github.com/jsonrainbow/json-schema/pull/704)) 81 | - Add contributing information, contributor recognition and security information ([#771](https://github.com/jsonrainbow/json-schema/pull/771)) 82 | 83 | ### Fixed 84 | - Correct misconfigured mocks in JsonSchema\Tests\Uri\UriRetrieverTest ([#741](https://github.com/jsonrainbow/json-schema/pull/741)) 85 | - Fix pugx badges in README ([#742](https://github.com/jsonrainbow/json-schema/pull/742)) 86 | - Add missing property in UriResolverTest ([#743](https://github.com/jsonrainbow/json-schema/pull/743)) 87 | - Correct casing of paths used in tests ([#745](https://github.com/jsonrainbow/json-schema/pull/745)) 88 | - Resolve deprecations of optional parameter ([#752](https://github.com/jsonrainbow/json-schema/pull/752)) 89 | - Fix wrong combined paths when traversing upward, fixes #557 ([#652](https://github.com/jsonrainbow/json-schema/pull/652)) 90 | - Correct PHPStan baseline ([#764](https://github.com/jsonrainbow/json-schema/pull/764)) 91 | - Correct spacing issue in `README.md` ([#763](https://github.com/jsonrainbow/json-schema/pull/763)) 92 | - Format attribute: do not validate data instances that aren't the instance type to validate ([#773](https://github.com/jsonrainbow/json-schema/pull/773)) 93 | 94 | ### Changed 95 | - Bump to minimum PHP 7.2 ([#746](https://github.com/jsonrainbow/json-schema/pull/746)) 96 | - Replace traditional syntax array with short syntax array ([#747](https://github.com/jsonrainbow/json-schema/pull/747)) 97 | - Increase phpstan level to 8 with baseline to swallow existing errors ([#673](https://github.com/jsonrainbow/json-schema/pull/673)) 98 | - Add ext-json to composer.json to ensure JSON extension available ([#759](https://github.com/jsonrainbow/json-schema/pull/759)) 99 | - Add visibility modifiers to class constants ([#757](https://github.com/jsonrainbow/json-schema/pull/757)) 100 | - Include PHP 8.4 in workflow ([#765](https://github.com/jsonrainbow/json-schema/pull/765)) 101 | - Add `strict_types=1` to all classes in ./src ([#758](https://github.com/jsonrainbow/json-schema/pull/758)) 102 | - Raise minimum level of marc-mabe/php-enum ([#766](https://github.com/jsonrainbow/json-schema/pull/766)) 103 | - Cleanup test from @param annotations ([#768](https://github.com/jsonrainbow/json-schema/pull/768)) 104 | - Remove obsolete PHP 7.1 version check ([#772](https://github.com/jsonrainbow/json-schema/pull/772)) 105 | 106 | ## [6.0.0] - 2024-07-30 107 | ### Added 108 | - Add URI translation, package:// URI scheme & bundle spec schemas ([#362](https://github.com/jsonrainbow/json-schema/pull/362)) 109 | - Add quiet option ([#382](https://github.com/jsonrainbow/json-schema/pull/382)) 110 | - Add option to disable validation of "format" constraint ([#383](https://github.com/jsonrainbow/json-schema/pull/383)) 111 | - Add more unit tests ([#366](https://github.com/jsonrainbow/json-schema/pull/366)) 112 | - Reset errors prior to validation ([#386](https://github.com/jsonrainbow/json-schema/pull/386)) 113 | - Allow the schema to be an associative array ([#389](https://github.com/jsonrainbow/json-schema/pull/389)) 114 | - Enable FILTER_FLAG_EMAIL_UNICODE for email format if present ([#398](https://github.com/jsonrainbow/json-schema/pull/398)) 115 | - Add enum wrapper ([#375](https://github.com/jsonrainbow/json-schema/pull/375)) 116 | - Add option to validate the schema ([#357](https://github.com/jsonrainbow/json-schema/pull/357)) 117 | - Add support for "const" ([#507](https://github.com/jsonrainbow/json-schema/pull/507)) 118 | - Added note about supported Draft versions ([#620](https://github.com/jsonrainbow/json-schema/pull/620)) 119 | - Add linting GH action 120 | ### Changed 121 | - Centralize errors ([#364](https://github.com/jsonrainbow/json-schema/pull/364)) 122 | - Revert "An email is a string, not much else." ([#373](https://github.com/jsonrainbow/json-schema/pull/373)) 123 | - Improvements to type coercion ([#384](https://github.com/jsonrainbow/json-schema/pull/384)) 124 | - Don't add a file:// prefix to URI that already have a scheme ([#455](https://github.com/jsonrainbow/json-schema/pull/455)) 125 | - Enhancement: Normalize` composer.json` ([#505](https://github.com/jsonrainbow/json-schema/pull/505)) 126 | - Correct echo `sprintf` for `printf` ([#634](https://github.com/jsonrainbow/json-schema/pull/634)) 127 | - Streamline validation of Regex ([#650](https://github.com/jsonrainbow/json-schema/pull/650)) 128 | - Streamline validation of patternProperties Regex ([#653](https://github.com/jsonrainbow/json-schema/pull/653)) 129 | - Switch to GH Actions ([#670](https://github.com/jsonrainbow/json-schema/pull/670)) 130 | - Updated PHPStan 131 | - Remove unwanted whitespace ([#700](https://github.com/jsonrainbow/json-schema/pull/700)) 132 | - Bump to v4 versions of GitHub actions ([#722](https://github.com/jsonrainbow/json-schema/pull/722)) 133 | - Update references to jsonrainbow ([#725](https://github.com/jsonrainbow/json-schema/pull/725)) 134 | ### Deprecated 135 | - Mark check() and coerce() as deprecated ([#476](https://github.com/jsonrainbow/json-schema/pull/476)) 136 | ### Removed 137 | - Remove stale files from #357 (obviated by #362) ([#400](https://github.com/jsonrainbow/json-schema/pull/400)) 138 | - Remove unnecessary fallbacks when args accept null 139 | - Removed unused variable in UndefinedConstraint ([#698](https://github.com/jsonrainbow/json-schema/pull/698)) 140 | - Remove dead block of code ([#710](https://github.com/jsonrainbow/json-schema/pull/710)) 141 | ### Fixed 142 | - Add use line for InvalidArgumentException ([#370](https://github.com/jsonrainbow/json-schema/pull/370)) 143 | - Add use line for InvalidArgumentException & adjust scope ([#372](https://github.com/jsonrainbow/json-schema/pull/372)) 144 | - Add provided schema under a dummy / internal URI (fixes #376) ([#378](https://github.com/jsonrainbow/json-schema/pull/378)) 145 | - Don't throw exceptions until after checking anyOf / oneOf ([#394](https://github.com/jsonrainbow/json-schema/pull/394)) 146 | - Fix infinite recursion on some schemas when setting defaults (#359) ([#365](https://github.com/jsonrainbow/json-schema/pull/365)) 147 | - Fix autoload to work properly with composer dependencies ([#401](https://github.com/jsonrainbow/json-schema/pull/401)) 148 | - Ignore $ref siblings & abort on infinite-loop references ([#437](https://github.com/jsonrainbow/json-schema/pull/437)) 149 | - Don't cast multipleOf to be an integer for the error message ([#471](https://github.com/jsonrainbow/json-schema/pull/471)) 150 | - Strict Enum/Const Object Checking ([#518](https://github.com/jsonrainbow/json-schema/pull/518)) 151 | - Return original value when no cast ([#535](https://github.com/jsonrainbow/json-schema/pull/535)) 152 | - Allow `marc-mabe/php-enum` v2.x and v3.x. ([#464](https://github.com/jsonrainbow/json-schema/pull/464)) 153 | - Deprecated warning message on composer install command ([#614](https://github.com/jsonrainbow/json-schema/pull/614)) 154 | - Allow `marc-mabe/php-enum` v4.x ([#629](https://github.com/jsonrainbow/json-schema/pull/629)) 155 | - Fixed method convertJsonPointerIntoPropertyPath in wrong class ([#655](https://github.com/jsonrainbow/json-schema/pull/655)) 156 | - Fix type validation failing for "any" and false-y type wording ([#686](https://github.com/jsonrainbow/json-schema/pull/686)) 157 | - Correct code style 158 | - Fix: Clean up `.gitattributes` ([#687](https://github.com/jsonrainbow/json-schema/pull/687)) 159 | - Fix: Order `friendsofphp/php-cs-fixer` rules ([#688](https://github.com/jsonrainbow/json-schema/pull/688)) 160 | - HTTP to HTTPS redirection breaks remote reference resolution ([#709](https://github.com/jsonrainbow/json-schema/pull/709)) 161 | - Corrected several typos and code style issues 162 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to JSON Schema 2 | 3 | First off, thanks for taking the time to contribute! ❤️ 4 | 5 | All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 6 | 7 | > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: 8 | > - Star the project 9 | > - Tweet about it 10 | > - Refer this project in your project's readme 11 | > - Mention the project at local meetups and tell your friends/colleagues 12 | 13 | ## Table of Contents 14 | 15 | - [I Have a Question](#i-have-a-question) 16 | - [I Want To Contribute](#i-want-to-contribute) 17 | - [Reporting Bugs](#reporting-bugs) 18 | - [Suggesting Enhancements](#suggesting-enhancements) 19 | 20 | ## I Have a Question 21 | 22 | > If you want to ask a question, we assume that you have read the available [Documentation](https://github.com/jsonrainbow/json-schema/wiki). 23 | 24 | Before you ask a question, it is best to search for existing [Issues](https://github.com/jsonrainbow/json-schema/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. 25 | 26 | If you then still feel the need to ask a question and need clarification, we recommend the following: 27 | 28 | - Open an [Issue](https://github.com/jsonrainbow/json-schema/issues/new). 29 | - Provide as much context as you can about what you're running into. 30 | - Provide project and PHP version, depending on what seems relevant. 31 | 32 | We will then take care of the issue as soon as possible. 33 | 34 | ## I Want To Contribute 35 | 36 | > ### Legal Notice 37 | > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project licence. 38 | 39 | ## Reporting Bugs 40 | 41 | ### Before Submitting a Bug Report 42 | 43 | A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. 44 | 45 | - Make sure that you are using the latest version. 46 | - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://github.com/jsonrainbow/json-schema/wiki). If you are looking for support, you might want to check [this section](#i-have-a-question)). 47 | - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/jsonrainbow/json-schema/issues?q=label%3Abug). 48 | - Collect information about the bug: 49 | - Stack trace (Traceback) 50 | - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) 51 | - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. 52 | - Possibly your input and the output 53 | - Can you reliably reproduce the issue? And can you also reproduce it with older versions? 54 | 55 | ### How Do I Submit a Good Bug Report? 56 | 57 | > You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead, sensitive bugs must be reported at https://github.com/jsonrainbow/json-schema/security 58 | 59 | GitHub issues is used to track bugs and errors. If you run into an issue with the project: 60 | 61 | - Open an [Issue](https://github.com/jsonrainbow/json-schema/issues/new). 62 | - Explain the behavior you would expect and the actual behavior. 63 | - Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. 64 | - Provide the information you collected in the previous section. 65 | 66 | Once it's filed: 67 | 68 | - The project team will label the issue accordingly. 69 | - A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. 70 | - If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution). 71 | 72 | ### Suggesting Enhancements 73 | 74 | This section guides you through submitting an enhancement suggestion for JSON Schema, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. 75 | 76 | #### Before Submitting an Enhancement 77 | 78 | - Make sure that you are using the latest version. 79 | - Read the [documentation](https://github.com/jsonrainbow/json-schema/wiki) carefully and find out if the functionality is already covered, maybe by an individual configuration. 80 | - Perform a [search](https://github.com/jsonrainbow/json-schema/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. 81 | - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. 82 | 83 | #### How Do I Submit a Good Enhancement Suggestion? 84 | 85 | Enhancement suggestions are tracked as [GitHub issues](https://github.com/jsonrainbow/json-schema/issues). 86 | 87 | - Use a **clear and descriptive title** for the issue to identify the suggestion. 88 | - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. 89 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. 90 | - You may want to **include screenshots or screen recordings** which help you demonstrate the steps or point out the part which the suggestion is related to. 91 | - **Explain why this enhancement would be useful** to most JSON Schema users. You may also want to point out the other projects that solved it better and which could serve as inspiration. 92 | 93 | 94 | 95 | ## Attribution 96 | This guide is based on the [contributing.md](https://contributing.md/generator)! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Schema for PHP 2 | 3 | [![Build Status](https://github.com/jsonrainbow/json-schema/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/jsonrainbow/json-schema/actions) 4 | [![Latest Stable Version](https://poser.pugx.org/justinrainbow/json-schema/v/stable)](https://packagist.org/packages/justinrainbow/json-schema) 5 | [![Total Downloads](https://poser.pugx.org/justinrainbow/json-schema/downloads)](https://packagist.org/packages/justinrainbow/json-schema/stats) 6 | 7 | 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. 8 | 9 | See [json-schema](http://json-schema.org/) for more details. 10 | 11 | ## Installation 12 | 13 | ### Library 14 | 15 | ```bash 16 | git clone https://github.com/jsonrainbow/json-schema.git 17 | ``` 18 | 19 | ### Composer 20 | 21 | [Install PHP Composer](https://getcomposer.org/doc/00-intro.md) 22 | 23 | ```bash 24 | composer require justinrainbow/json-schema 25 | ``` 26 | 27 | ## Usage 28 | 29 | For a complete reference see [Understanding JSON Schema](https://json-schema.org/understanding-json-schema/). 30 | 31 | __Note:__ features of Drafts newer than Draft-4 might not be supported! 32 | 33 | ### Basic usage 34 | 35 | ```php 36 | validate($data, (object)['$ref' => 'file://' . realpath('schema.json')]); 43 | 44 | if ($validator->isValid()) { 45 | echo "The supplied JSON validates against the schema.\n"; 46 | } else { 47 | echo "JSON does not validate. Violations:\n"; 48 | foreach ($validator->getErrors() as $error) { 49 | printf("[%s] %s\n", $error['property'], $error['message']); 50 | } 51 | } 52 | ``` 53 | 54 | ### Type coercion 55 | 56 | If you're validating data passed to your application via HTTP, you can cast strings and booleans to 57 | the expected types defined by your schema: 58 | 59 | ```php 60 | "true", 69 | 'refundAmount'=>"17" 70 | ]; 71 | 72 | $validator->validate( 73 | $request, (object) [ 74 | "type"=>"object", 75 | "properties"=>(object)[ 76 | "processRefund"=>(object)[ 77 | "type"=>"boolean" 78 | ], 79 | "refundAmount"=>(object)[ 80 | "type"=>"number" 81 | ] 82 | ] 83 | ], 84 | Constraint::CHECK_MODE_COERCE_TYPES 85 | ); // validates! 86 | 87 | is_bool($request->processRefund); // true 88 | is_int($request->refundAmount); // true 89 | ``` 90 | 91 | A shorthand method is also available: 92 | ```PHP 93 | $validator->coerce($request, $schema); 94 | // equivalent to $validator->validate($data, $schema, Constraint::CHECK_MODE_COERCE_TYPES); 95 | ``` 96 | 97 | ### Default values 98 | 99 | If your schema contains default values, you can have these automatically applied during validation: 100 | 101 | ```php 102 | 17 109 | ]; 110 | 111 | $validator = new Validator(); 112 | 113 | $validator->validate( 114 | $request, 115 | (object)[ 116 | "type"=>"object", 117 | "properties"=>(object)[ 118 | "processRefund"=>(object)[ 119 | "type"=>"boolean", 120 | "default"=>true 121 | ] 122 | ] 123 | ], 124 | Constraint::CHECK_MODE_APPLY_DEFAULTS 125 | ); //validates, and sets defaults for missing properties 126 | 127 | is_bool($request->processRefund); // true 128 | $request->processRefund; // true 129 | ``` 130 | 131 | ### With inline references 132 | 133 | ```php 134 | addSchema('file://mySchema', $jsonSchemaObject); 174 | 175 | // Provide $schemaStorage to the Validator so that references can be resolved during validation 176 | $jsonValidator = new Validator(new Factory($schemaStorage)); 177 | 178 | // JSON must be decoded before it can be validated 179 | $jsonToValidateObject = json_decode('{"data":123}'); 180 | 181 | // Do validation (use isValid() and getErrors() to check the result) 182 | $jsonValidator->validate($jsonToValidateObject, $jsonSchemaObject); 183 | ``` 184 | 185 | ### Configuration Options 186 | A number of flags are available to alter the behavior of the validator. These can be passed as the 187 | third argument to `Validator::validate()`, or can be provided as the third argument to 188 | `Factory::__construct()` if you wish to persist them across multiple `validate()` calls. 189 | 190 | | Flag | Description | 191 | |------|-------------| 192 | | `Constraint::CHECK_MODE_NORMAL` | Validate in 'normal' mode - this is the default | 193 | | `Constraint::CHECK_MODE_TYPE_CAST` | Enable fuzzy type checking for associative arrays and objects | 194 | | `Constraint::CHECK_MODE_COERCE_TYPES` | Convert data types to match the schema where possible | 195 | | `Constraint::CHECK_MODE_EARLY_COERCE` | Apply type coercion as soon as possible | 196 | | `Constraint::CHECK_MODE_APPLY_DEFAULTS` | Apply default values from the schema if not set | 197 | | `Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS` | When applying defaults, only set values that are required | 198 | | `Constraint::CHECK_MODE_EXCEPTIONS` | Throw an exception immediately if validation fails | 199 | | `Constraint::CHECK_MODE_DISABLE_FORMAT` | Do not validate "format" constraints | 200 | | `Constraint::CHECK_MODE_VALIDATE_SCHEMA` | Validate the schema as well as the provided document | 201 | 202 | Please note that using `CHECK_MODE_COERCE_TYPES` or `CHECK_MODE_APPLY_DEFAULTS` will modify your 203 | original data. 204 | 205 | `CHECK_MODE_EARLY_COERCE` has no effect unless used in combination with `CHECK_MODE_COERCE_TYPES`. If 206 | enabled, the validator will use (and coerce) the first compatible type it encounters, even if the 207 | schema defines another type that matches directly and does not require coercion. 208 | 209 | ## Running the tests 210 | 211 | ```bash 212 | composer test # run all unit tests 213 | composer testOnly TestClass # run specific unit test class 214 | composer testOnly TestClass::testMethod # run specific unit test method 215 | composer style-check # check code style for errors 216 | composer style-fix # automatically fix code style errors 217 | ``` 218 | 219 | # Contributors ✨ 220 | Thanks go to these wonderful people, without their effort this project wasn't possible. 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 6.x.x | :white_check_mark: | 8 | | 5.x.x | :white_check_mark: | 9 | | < 5.0 | :x: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | JSON Schema uses the GitHub feature to safely report vulnerabilities, please report them using https://github.com/jsonrainbow/json-schema/security 14 | -------------------------------------------------------------------------------- /UPGRADE-6.0.md: -------------------------------------------------------------------------------- 1 | UPGRADE FROM 5.3 to 6.0 2 | ======================= 3 | 4 | ## Introduction 5 | 6 | We are excited to release version 6.0 of our open-source package, featuring major improvements and important updates. This release includes several breaking changes from version 5.3 aimed at enhancing performance, security, and flexibility. 7 | 8 | Please review the following breaking changes carefully and update your implementations to ensure compatibility with version 6.0. This guide provides key modifications and instructions for a smooth transition. 9 | 10 | Thank you for your support and contributions to the project. 11 | 12 | ## Errors 13 | * `constraint` key is no longer the constraint name but contains more information in order to translate violations. 14 | 15 | *Before* 16 | ```php 17 | foreach ($validator->getErrors() as $error) { 18 | echo $error['constraint']; // required 19 | } 20 | ``` 21 | 22 | *After* 23 | ```php 24 | foreach ($validator->getErrors() as $error) { 25 | echo $error['constraint']['name']; // required 26 | } 27 | ``` 28 | 29 | ## BaseConstraint::addError signature changed 30 | 31 | * The signature for the `BaseConstraint::AddError` method has changed. 32 | 33 | The `$message` parameter has been removed and replaced by the `ConstraintError` parameter. 34 | The `ConstraintError` object encapsulates the error message along with additional information about the constraint violation. 35 | 36 | *Before* 37 | ```php 38 | public function addError(?JsonPointer $path, $message, $constraint = '', ?array $more = null) 39 | ``` 40 | 41 | *After* 42 | ```php 43 | public function addError(ConstraintError $constraint, ?JsonPointer $path = null, array $more = []): void 44 | ``` 45 | 46 | -------------------------------------------------------------------------------- /bin/validate-json: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 7 | */ 8 | 9 | // support running this tool from git checkout 10 | $projectDirectory = dirname(__DIR__); 11 | if (is_dir($projectDirectory . DIRECTORY_SEPARATOR . 'vendor')) { 12 | set_include_path($projectDirectory . PATH_SEPARATOR . get_include_path()); 13 | } 14 | 15 | // autoload composer classes 16 | $composerAutoload = stream_resolve_include_path('vendor/autoload.php'); 17 | if (!$composerAutoload) { 18 | echo("Cannot load json-schema library\n"); 19 | exit(1); 20 | } 21 | require($composerAutoload); 22 | 23 | $arOptions = []; 24 | $arArgs = []; 25 | array_shift($argv);//script itself 26 | foreach ($argv as $arg) { 27 | if ($arg[0] == '-') { 28 | $arOptions[$arg] = true; 29 | } else { 30 | $arArgs[] = $arg; 31 | } 32 | } 33 | 34 | if (count($arArgs) == 0 35 | || isset($arOptions['--help']) || isset($arOptions['-h']) 36 | ) { 37 | echo << $value) { 69 | if (!strncmp($name, 'JSON_ERROR_', 11)) { 70 | $json_errors[$value] = $name; 71 | } 72 | } 73 | 74 | output('JSON parse error: ' . $json_errors[json_last_error()] . "\n"); 75 | } 76 | 77 | function getUrlFromPath($path) 78 | { 79 | if (parse_url($path, PHP_URL_SCHEME) !== null) { 80 | //already an URL 81 | return $path; 82 | } 83 | if ($path[0] == '/') { 84 | //absolute path 85 | return 'file://' . $path; 86 | } 87 | 88 | //relative path: make absolute 89 | return 'file://' . getcwd() . '/' . $path; 90 | } 91 | 92 | /** 93 | * Take a HTTP header value and split it up into parts. 94 | * 95 | * @param $headerValue 96 | * @return array Key "_value" contains the main value, all others 97 | * as given in the header value 98 | */ 99 | function parseHeaderValue($headerValue) 100 | { 101 | if (strpos($headerValue, ';') === false) { 102 | return ['_value' => $headerValue]; 103 | } 104 | 105 | $parts = explode(';', $headerValue); 106 | $arData = ['_value' => array_shift($parts)]; 107 | foreach ($parts as $part) { 108 | list($name, $value) = explode('=', $part); 109 | $arData[$name] = trim($value, ' "\''); 110 | } 111 | return $arData; 112 | } 113 | 114 | /** 115 | * Send a string to the output stream, but only if --quiet is not enabled 116 | * 117 | * @param $str string A string output 118 | */ 119 | function output($str) { 120 | global $arOptions; 121 | if (!isset($arOptions['--quiet'])) { 122 | echo $str; 123 | } 124 | } 125 | 126 | $urlData = getUrlFromPath($pathData); 127 | 128 | $context = stream_context_create( 129 | [ 130 | 'http' => [ 131 | 'header' => [ 132 | 'Accept: */*', 133 | 'Connection: Close' 134 | ], 135 | 'max_redirects' => 5 136 | ] 137 | ] 138 | ); 139 | $dataString = file_get_contents($pathData, false, $context); 140 | if ($dataString == '') { 141 | output("Data file is not readable or empty.\n"); 142 | exit(3); 143 | } 144 | 145 | $data = json_decode($dataString); 146 | unset($dataString); 147 | if ($data === null) { 148 | output("Error loading JSON data file\n"); 149 | showJsonError(); 150 | exit(5); 151 | } 152 | 153 | if ($pathSchema === null) { 154 | if (isset($http_response_header)) { 155 | array_shift($http_response_header);//HTTP/1.0 line 156 | foreach ($http_response_header as $headerLine) { 157 | list($hName, $hValue) = explode(':', $headerLine, 2); 158 | $hName = strtolower($hName); 159 | if ($hName == 'link') { 160 | //Link: ; rel="describedBy" 161 | $hParts = parseHeaderValue($hValue); 162 | if (isset($hParts['rel']) && $hParts['rel'] == 'describedBy') { 163 | $pathSchema = trim($hParts['_value'], ' <>'); 164 | } 165 | } else if ($hName == 'content-type') { 166 | //Content-Type: application/my-media-type+json; 167 | // profile=http://example.org/schema# 168 | $hParts = parseHeaderValue($hValue); 169 | if (isset($hParts['profile'])) { 170 | $pathSchema = $hParts['profile']; 171 | } 172 | 173 | } 174 | } 175 | } 176 | if (is_object($data) && property_exists($data, '$schema')) { 177 | $pathSchema = $data->{'$schema'}; 178 | } 179 | 180 | //autodetect schema 181 | if ($pathSchema === null) { 182 | output("JSON data must be an object and have a \$schema property.\n"); 183 | output("You can pass the schema file on the command line as well.\n"); 184 | output("Schema autodetection failed.\n"); 185 | exit(6); 186 | } 187 | } 188 | if ($pathSchema[0] == '/') { 189 | $pathSchema = 'file://' . $pathSchema; 190 | } 191 | 192 | $resolver = new JsonSchema\Uri\UriResolver(); 193 | $retriever = new JsonSchema\Uri\UriRetriever(); 194 | try { 195 | $urlSchema = $resolver->resolve($pathSchema, $urlData); 196 | 197 | if (isset($arOptions['--dump-schema-url'])) { 198 | echo $urlSchema . "\n"; 199 | exit(); 200 | } 201 | } catch (Exception $e) { 202 | output("Error loading JSON schema file\n"); 203 | output($urlSchema . "\n"); 204 | output($e->getMessage() . "\n"); 205 | exit(2); 206 | } 207 | $refResolver = new JsonSchema\SchemaStorage($retriever, $resolver); 208 | $schema = $refResolver->resolveRef($urlSchema); 209 | 210 | if (isset($arOptions['--dump-schema'])) { 211 | $options = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0; 212 | echo json_encode($schema, $options) . "\n"; 213 | exit(); 214 | } 215 | 216 | try { 217 | $validator = new JsonSchema\Validator(); 218 | $validator->validate($data, $schema); 219 | 220 | if ($validator->isValid()) { 221 | if(isset($arOptions['--verbose'])) { 222 | output("OK. The supplied JSON validates against the schema.\n"); 223 | } 224 | } else { 225 | output("JSON does not validate. Violations:\n"); 226 | foreach ($validator->getErrors() as $error) { 227 | output(sprintf("[%s] %s\n", $error['property'], $error['message'])); 228 | } 229 | exit(23); 230 | } 231 | } catch (Exception $e) { 232 | output("JSON does not validate. Error:\n"); 233 | output($e->getMessage() . "\n"); 234 | output("Error code: " . $e->getCode() . "\n"); 235 | exit(24); 236 | } 237 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "justinrainbow/json-schema", 3 | "type": "library", 4 | "description": "A library to validate a json schema.", 5 | "keywords": [ 6 | "json", 7 | "schema" 8 | ], 9 | "homepage": "https://github.com/jsonrainbow/json-schema", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Bruno Prieto Reis", 14 | "email": "bruno.p.reis@gmail.com" 15 | }, 16 | { 17 | "name": "Justin Rainbow", 18 | "email": "justin.rainbow@gmail.com" 19 | }, 20 | { 21 | "name": "Igor Wiedler", 22 | "email": "igor@wiedler.ch" 23 | }, 24 | { 25 | "name": "Robert Schönthal", 26 | "email": "seroscho@googlemail.com" 27 | } 28 | ], 29 | "require": { 30 | "php": "^7.2 || ^8.0", 31 | "ext-json": "*", 32 | "marc-mabe/php-enum":"^4.0" 33 | }, 34 | "require-dev": { 35 | "friendsofphp/php-cs-fixer": "3.3.0", 36 | "json-schema/json-schema-test-suite": "1.2.0", 37 | "phpunit/phpunit": "^8.5", 38 | "phpspec/prophecy": "^1.19", 39 | "phpstan/phpstan": "^1.12", 40 | "marc-mabe/php-enum-phpstan": "^2.0" 41 | }, 42 | "extra": { 43 | "branch-alias": { 44 | "dev-master": "6.x-dev" 45 | } 46 | }, 47 | "autoload": { 48 | "psr-4": { 49 | "JsonSchema\\": "src/JsonSchema/" 50 | } 51 | }, 52 | "autoload-dev": { 53 | "psr-4": { 54 | "JsonSchema\\Tests\\": "tests/" 55 | } 56 | }, 57 | "repositories": [ 58 | { 59 | "type": "package", 60 | "package": { 61 | "name": "json-schema/json-schema-test-suite", 62 | "version": "1.2.0", 63 | "source": { 64 | "type": "git", 65 | "url": "https://github.com/json-schema/JSON-Schema-Test-Suite", 66 | "reference": "1.2.0" 67 | } 68 | } 69 | } 70 | ], 71 | "bin": [ 72 | "bin/validate-json" 73 | ], 74 | "scripts": { 75 | "coverage": "phpunit --coverage-text", 76 | "style-check": "php-cs-fixer fix --dry-run --verbose --diff", 77 | "style-fix": "php-cs-fixer fix --verbose", 78 | "test": "phpunit", 79 | "testOnly": "phpunit --colors --filter", 80 | "phpstan": "@php phpstan", 81 | "phpstan-generate-baseline": "@php phpstan --generate-baseline" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /dist/schema/json-schema-draft-03.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-03/schema#", 3 | "id": "http://json-schema.org/draft-03/schema#", 4 | "type": "object", 5 | 6 | "properties": { 7 | "type": { 8 | "type": [ "string", "array" ], 9 | "items": { 10 | "type": [ "string", { "$ref": "#" } ] 11 | }, 12 | "uniqueItems": true, 13 | "default": "any" 14 | }, 15 | 16 | "properties": { 17 | "type": "object", 18 | "additionalProperties": { "$ref": "#" }, 19 | "default": {} 20 | }, 21 | 22 | "patternProperties": { 23 | "type": "object", 24 | "additionalProperties": { "$ref": "#" }, 25 | "default": {} 26 | }, 27 | 28 | "additionalProperties": { 29 | "type": [ { "$ref": "#" }, "boolean" ], 30 | "default": {} 31 | }, 32 | 33 | "items": { 34 | "type": [ { "$ref": "#" }, "array" ], 35 | "items": { "$ref": "#" }, 36 | "default": {} 37 | }, 38 | 39 | "additionalItems": { 40 | "type": [ { "$ref": "#" }, "boolean" ], 41 | "default": {} 42 | }, 43 | 44 | "required": { 45 | "type": "boolean", 46 | "default": false 47 | }, 48 | 49 | "dependencies": { 50 | "type": "object", 51 | "additionalProperties": { 52 | "type": [ "string", "array", { "$ref": "#" } ], 53 | "items": { 54 | "type": "string" 55 | } 56 | }, 57 | "default": {} 58 | }, 59 | 60 | "minimum": { 61 | "type": "number" 62 | }, 63 | 64 | "maximum": { 65 | "type": "number" 66 | }, 67 | 68 | "exclusiveMinimum": { 69 | "type": "boolean", 70 | "default": false 71 | }, 72 | 73 | "exclusiveMaximum": { 74 | "type": "boolean", 75 | "default": false 76 | }, 77 | 78 | "minItems": { 79 | "type": "integer", 80 | "minimum": 0, 81 | "default": 0 82 | }, 83 | 84 | "maxItems": { 85 | "type": "integer", 86 | "minimum": 0 87 | }, 88 | 89 | "uniqueItems": { 90 | "type": "boolean", 91 | "default": false 92 | }, 93 | 94 | "pattern": { 95 | "type": "string", 96 | "format": "regex" 97 | }, 98 | 99 | "minLength": { 100 | "type": "integer", 101 | "minimum": 0, 102 | "default": 0 103 | }, 104 | 105 | "maxLength": { 106 | "type": "integer" 107 | }, 108 | 109 | "enum": { 110 | "type": "array", 111 | "minItems": 1, 112 | "uniqueItems": true 113 | }, 114 | 115 | "default": { 116 | "type": "any" 117 | }, 118 | 119 | "title": { 120 | "type": "string" 121 | }, 122 | 123 | "description": { 124 | "type": "string" 125 | }, 126 | 127 | "format": { 128 | "type": "string" 129 | }, 130 | 131 | "divisibleBy": { 132 | "type": "number", 133 | "minimum": 0, 134 | "exclusiveMinimum": true, 135 | "default": 1 136 | }, 137 | 138 | "disallow": { 139 | "type": [ "string", "array" ], 140 | "items": { 141 | "type": [ "string", { "$ref": "#" } ] 142 | }, 143 | "uniqueItems": true 144 | }, 145 | 146 | "extends": { 147 | "type": [ { "$ref": "#" }, "array" ], 148 | "items": { "$ref": "#" }, 149 | "default": {} 150 | }, 151 | 152 | "id": { 153 | "type": "string", 154 | "format": "uri" 155 | }, 156 | 157 | "$ref": { 158 | "type": "string", 159 | "format": "uri" 160 | }, 161 | 162 | "$schema": { 163 | "type": "string", 164 | "format": "uri" 165 | } 166 | }, 167 | 168 | "dependencies": { 169 | "exclusiveMinimum": "minimum", 170 | "exclusiveMaximum": "maximum" 171 | }, 172 | 173 | "default": {} 174 | } 175 | -------------------------------------------------------------------------------- /dist/schema/json-schema-draft-04.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://json-schema.org/draft-04/schema#", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "description": "Core schema meta-schema", 5 | "definitions": { 6 | "schemaArray": { 7 | "type": "array", 8 | "minItems": 1, 9 | "items": { "$ref": "#" } 10 | }, 11 | "positiveInteger": { 12 | "type": "integer", 13 | "minimum": 0 14 | }, 15 | "positiveIntegerDefault0": { 16 | "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] 17 | }, 18 | "simpleTypes": { 19 | "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] 20 | }, 21 | "stringArray": { 22 | "type": "array", 23 | "items": { "type": "string" }, 24 | "minItems": 1, 25 | "uniqueItems": true 26 | } 27 | }, 28 | "type": "object", 29 | "properties": { 30 | "id": { 31 | "type": "string", 32 | "format": "uri" 33 | }, 34 | "$schema": { 35 | "type": "string", 36 | "format": "uri" 37 | }, 38 | "title": { 39 | "type": "string" 40 | }, 41 | "description": { 42 | "type": "string" 43 | }, 44 | "default": {}, 45 | "multipleOf": { 46 | "type": "number", 47 | "minimum": 0, 48 | "exclusiveMinimum": true 49 | }, 50 | "maximum": { 51 | "type": "number" 52 | }, 53 | "exclusiveMaximum": { 54 | "type": "boolean", 55 | "default": false 56 | }, 57 | "minimum": { 58 | "type": "number" 59 | }, 60 | "exclusiveMinimum": { 61 | "type": "boolean", 62 | "default": false 63 | }, 64 | "maxLength": { "$ref": "#/definitions/positiveInteger" }, 65 | "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, 66 | "pattern": { 67 | "type": "string", 68 | "format": "regex" 69 | }, 70 | "additionalItems": { 71 | "anyOf": [ 72 | { "type": "boolean" }, 73 | { "$ref": "#" } 74 | ], 75 | "default": {} 76 | }, 77 | "items": { 78 | "anyOf": [ 79 | { "$ref": "#" }, 80 | { "$ref": "#/definitions/schemaArray" } 81 | ], 82 | "default": {} 83 | }, 84 | "maxItems": { "$ref": "#/definitions/positiveInteger" }, 85 | "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, 86 | "uniqueItems": { 87 | "type": "boolean", 88 | "default": false 89 | }, 90 | "maxProperties": { "$ref": "#/definitions/positiveInteger" }, 91 | "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, 92 | "required": { "$ref": "#/definitions/stringArray" }, 93 | "additionalProperties": { 94 | "anyOf": [ 95 | { "type": "boolean" }, 96 | { "$ref": "#" } 97 | ], 98 | "default": {} 99 | }, 100 | "definitions": { 101 | "type": "object", 102 | "additionalProperties": { "$ref": "#" }, 103 | "default": {} 104 | }, 105 | "properties": { 106 | "type": "object", 107 | "additionalProperties": { "$ref": "#" }, 108 | "default": {} 109 | }, 110 | "patternProperties": { 111 | "type": "object", 112 | "additionalProperties": { "$ref": "#" }, 113 | "default": {} 114 | }, 115 | "dependencies": { 116 | "type": "object", 117 | "additionalProperties": { 118 | "anyOf": [ 119 | { "$ref": "#" }, 120 | { "$ref": "#/definitions/stringArray" } 121 | ] 122 | } 123 | }, 124 | "enum": { 125 | "type": "array", 126 | "minItems": 1, 127 | "uniqueItems": true 128 | }, 129 | "type": { 130 | "anyOf": [ 131 | { "$ref": "#/definitions/simpleTypes" }, 132 | { 133 | "type": "array", 134 | "items": { "$ref": "#/definitions/simpleTypes" }, 135 | "minItems": 1, 136 | "uniqueItems": true 137 | } 138 | ] 139 | }, 140 | "allOf": { "$ref": "#/definitions/schemaArray" }, 141 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 142 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 143 | "not": { "$ref": "#" } 144 | }, 145 | "dependencies": { 146 | "exclusiveMaximum": [ "maximum" ], 147 | "exclusiveMinimum": [ "minimum" ] 148 | }, 149 | "default": {} 150 | } 151 | -------------------------------------------------------------------------------- /src/JsonSchema/ConstraintError.php: -------------------------------------------------------------------------------- 1 | getValue(); 63 | static $messages = [ 64 | self::ADDITIONAL_ITEMS => 'The item %s[%s] is not defined and the definition does not allow additional items', 65 | self::ADDITIONAL_PROPERTIES => 'The property %s is not defined and the definition does not allow additional properties', 66 | self::ALL_OF => 'Failed to match all schemas', 67 | self::ANY_OF => 'Failed to match at least one schema', 68 | self::DEPENDENCIES => '%s depends on %s, which is missing', 69 | self::DISALLOW => 'Disallowed value was matched', 70 | self::DIVISIBLE_BY => 'Is not divisible by %d', 71 | self::ENUM => 'Does not have a value in the enumeration %s', 72 | self::CONSTANT => 'Does not have a value equal to %s', 73 | self::EXCLUSIVE_MINIMUM => 'Must have a minimum value greater than %d', 74 | self::EXCLUSIVE_MAXIMUM => 'Must have a maximum value less than %d', 75 | self::FORMAT_COLOR => 'Invalid color', 76 | self::FORMAT_DATE => 'Invalid date %s, expected format YYYY-MM-DD', 77 | self::FORMAT_DATE_TIME => 'Invalid date-time %s, expected format YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss+hh:mm', 78 | self::FORMAT_DATE_UTC => 'Invalid time %s, expected integer of milliseconds since Epoch', 79 | self::FORMAT_EMAIL => 'Invalid email', 80 | self::FORMAT_HOSTNAME => 'Invalid hostname', 81 | self::FORMAT_IP => 'Invalid IP address', 82 | self::FORMAT_PHONE => 'Invalid phone number', 83 | self::FORMAT_REGEX=> 'Invalid regex format %s', 84 | self::FORMAT_STYLE => 'Invalid style', 85 | self::FORMAT_TIME => 'Invalid time %s, expected format hh:mm:ss', 86 | self::FORMAT_URL => 'Invalid URL format', 87 | self::FORMAT_URL_REF => 'Invalid URL reference format', 88 | self::LENGTH_MAX => 'Must be at most %d characters long', 89 | self::INVALID_SCHEMA => 'Schema is not valid', 90 | self::LENGTH_MIN => 'Must be at least %d characters long', 91 | self::MAX_ITEMS => 'There must be a maximum of %d items in the array, %d found', 92 | self::MAXIMUM => 'Must have a maximum value less than or equal to %d', 93 | self::MIN_ITEMS => 'There must be a minimum of %d items in the array, %d found', 94 | self::MINIMUM => 'Must have a minimum value greater than or equal to %d', 95 | self::MISSING_MAXIMUM => 'Use of exclusiveMaximum requires presence of maximum', 96 | self::MISSING_MINIMUM => 'Use of exclusiveMinimum requires presence of minimum', 97 | /*self::MISSING_ERROR => 'Used for tests; this error is deliberately commented out',*/ 98 | self::MULTIPLE_OF => 'Must be a multiple of %s', 99 | self::NOT => 'Matched a schema which it should not', 100 | self::ONE_OF => 'Failed to match exactly one schema', 101 | self::REQUIRED => 'The property %s is required', 102 | self::REQUIRES => 'The presence of the property %s requires that %s also be present', 103 | self::PATTERN => 'Does not match the regex pattern %s', 104 | self::PREGEX_INVALID => 'The pattern %s is invalid', 105 | self::PROPERTIES_MIN => 'Must contain a minimum of %d properties', 106 | self::PROPERTIES_MAX => 'Must contain no more than %d properties', 107 | self::TYPE => '%s value found, but %s is required', 108 | self::UNIQUE_ITEMS => 'There are no duplicates allowed in the array' 109 | ]; 110 | 111 | if (!isset($messages[$name])) { 112 | throw new InvalidArgumentException('Missing error message for ' . $name); 113 | } 114 | 115 | return $messages[$name]; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/BaseConstraint.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | protected $errorMask = Validator::ERROR_NONE; 30 | 31 | /** 32 | * @var Factory 33 | */ 34 | protected $factory; 35 | 36 | public function __construct(?Factory $factory = null) 37 | { 38 | $this->factory = $factory ?: new Factory(); 39 | } 40 | 41 | public function addError(ConstraintError $constraint, ?JsonPointer $path = null, array $more = []): void 42 | { 43 | $message = $constraint->getMessage(); 44 | $name = $constraint->getValue(); 45 | $error = [ 46 | 'property' => $this->convertJsonPointerIntoPropertyPath($path ?: new JsonPointer('')), 47 | 'pointer' => ltrim((string) ($path ?: new JsonPointer('')), '#'), 48 | 'message' => ucfirst(vsprintf($message, array_map(static function ($val) { 49 | if (is_scalar($val)) { 50 | return is_bool($val) ? var_export($val, true) : $val; 51 | } 52 | 53 | return json_encode($val); 54 | }, array_values($more)))), 55 | 'constraint' => [ 56 | 'name' => $name, 57 | 'params' => $more 58 | ], 59 | 'context' => $this->factory->getErrorContext(), 60 | ]; 61 | 62 | if ($this->factory->getConfig(Constraint::CHECK_MODE_EXCEPTIONS)) { 63 | throw new ValidationException(sprintf('Error validating %s: %s', $error['pointer'], $error['message'])); 64 | } 65 | 66 | $this->errors[] = $error; 67 | $this->errorMask |= $error['context']; 68 | } 69 | 70 | public function addErrors(array $errors): void 71 | { 72 | if ($errors) { 73 | $this->errors = array_merge($this->errors, $errors); 74 | $errorMask = &$this->errorMask; 75 | array_walk($errors, static function ($error) use (&$errorMask) { 76 | if (isset($error['context'])) { 77 | $errorMask |= $error['context']; 78 | } 79 | }); 80 | } 81 | } 82 | 83 | /** 84 | * @phpstan-param int-mask-of $errorContext 85 | */ 86 | public function getErrors(int $errorContext = Validator::ERROR_ALL): array 87 | { 88 | if ($errorContext === Validator::ERROR_ALL) { 89 | return $this->errors; 90 | } 91 | 92 | return array_filter($this->errors, static function ($error) use ($errorContext) { 93 | return (bool) ($errorContext & $error['context']); 94 | }); 95 | } 96 | 97 | /** 98 | * @phpstan-param int-mask-of $errorContext 99 | */ 100 | public function numErrors(int $errorContext = Validator::ERROR_ALL): int 101 | { 102 | if ($errorContext === Validator::ERROR_ALL) { 103 | return count($this->errors); 104 | } 105 | 106 | return count($this->getErrors($errorContext)); 107 | } 108 | 109 | public function isValid(): bool 110 | { 111 | return !$this->getErrors(); 112 | } 113 | 114 | /** 115 | * Clears any reported errors. Should be used between 116 | * multiple validation checks. 117 | */ 118 | public function reset(): void 119 | { 120 | $this->errors = []; 121 | $this->errorMask = Validator::ERROR_NONE; 122 | } 123 | 124 | /** 125 | * Get the error mask 126 | * 127 | * @phpstan-return int-mask-of 128 | */ 129 | public function getErrorMask(): int 130 | { 131 | return $this->errorMask; 132 | } 133 | 134 | /** 135 | * Recursively cast an associative array to an object 136 | */ 137 | public static function arrayToObjectRecursive(array $array): object 138 | { 139 | $json = json_encode($array); 140 | if (json_last_error() !== JSON_ERROR_NONE) { 141 | $message = 'Unable to encode schema array as JSON'; 142 | if (function_exists('json_last_error_msg')) { 143 | $message .= ': ' . json_last_error_msg(); 144 | } 145 | throw new InvalidArgumentException($message); 146 | } 147 | 148 | return (object) json_decode($json, false); 149 | } 150 | 151 | /** 152 | * Transform a JSON pattern into a PCRE regex 153 | */ 154 | public static function jsonPatternToPhpRegex(string $pattern): string 155 | { 156 | return '~' . str_replace('~', '\\~', $pattern) . '~u'; 157 | } 158 | 159 | protected function convertJsonPointerIntoPropertyPath(JsonPointer $pointer): string 160 | { 161 | $result = array_map( 162 | static function ($path) { 163 | return sprintf(is_numeric($path) ? '[%d]' : '.%s', $path); 164 | }, 165 | $pointer->getPropertyPaths() 166 | ); 167 | 168 | return trim(implode('', $result), '.'); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/CollectionConstraint.php: -------------------------------------------------------------------------------- 1 | 22 | * @author Bruno Prieto Reis 23 | */ 24 | class CollectionConstraint extends Constraint 25 | { 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void 30 | { 31 | // Verify minItems 32 | if (isset($schema->minItems) && count($value) < $schema->minItems) { 33 | $this->addError(ConstraintError::MIN_ITEMS(), $path, ['minItems' => $schema->minItems, 'found' => count($value)]); 34 | } 35 | 36 | // Verify maxItems 37 | if (isset($schema->maxItems) && count($value) > $schema->maxItems) { 38 | $this->addError(ConstraintError::MAX_ITEMS(), $path, ['maxItems' => $schema->maxItems, 'found' => count($value)]); 39 | } 40 | 41 | // Verify uniqueItems 42 | if (isset($schema->uniqueItems) && $schema->uniqueItems) { 43 | $count = count($value); 44 | for ($x = 0; $x < $count - 1; $x++) { 45 | for ($y = $x + 1; $y < $count; $y++) { 46 | if (DeepComparer::isEqual($value[$x], $value[$y])) { 47 | $this->addError(ConstraintError::UNIQUE_ITEMS(), $path); 48 | break 2; 49 | } 50 | } 51 | } 52 | } 53 | 54 | $this->validateItems($value, $schema, $path, $i); 55 | } 56 | 57 | /** 58 | * Validates the items 59 | * 60 | * @param array $value 61 | * @param \stdClass $schema 62 | * @param string $i 63 | */ 64 | protected function validateItems(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void 65 | { 66 | if (\is_null($schema) || !isset($schema->items)) { 67 | return; 68 | } 69 | 70 | if ($schema->items === true) { 71 | return; 72 | } 73 | 74 | if (is_object($schema->items)) { 75 | // just one type definition for the whole array 76 | foreach ($value as $k => &$v) { 77 | $initErrors = $this->getErrors(); 78 | 79 | // First check if its defined in "items" 80 | $this->checkUndefined($v, $schema->items, $path, $k); 81 | 82 | // Recheck with "additionalItems" if the first test fails 83 | if (count($initErrors) < count($this->getErrors()) && (isset($schema->additionalItems) && $schema->additionalItems !== false)) { 84 | $secondErrors = $this->getErrors(); 85 | $this->checkUndefined($v, $schema->additionalItems, $path, $k); 86 | } 87 | 88 | // Reset errors if needed 89 | if (isset($secondErrors) && count($secondErrors) < count($this->getErrors())) { 90 | $this->errors = $secondErrors; 91 | } elseif (isset($secondErrors) && count($secondErrors) === count($this->getErrors())) { 92 | $this->errors = $initErrors; 93 | } 94 | } 95 | unset($v); /* remove dangling reference to prevent any future bugs 96 | * caused by accidentally using $v elsewhere */ 97 | } else { 98 | // Defined item type definitions 99 | foreach ($value as $k => &$v) { 100 | if (array_key_exists($k, $schema->items)) { 101 | $this->checkUndefined($v, $schema->items[$k], $path, $k); 102 | } else { 103 | // Additional items 104 | if (property_exists($schema, 'additionalItems')) { 105 | if ($schema->additionalItems !== false) { 106 | $this->checkUndefined($v, $schema->additionalItems, $path, $k); 107 | } else { 108 | $this->addError( 109 | ConstraintError::ADDITIONAL_ITEMS(), 110 | $path, 111 | [ 112 | 'item' => $i, 113 | 'property' => $k, 114 | 'additionalItems' => $schema->additionalItems 115 | ] 116 | ); 117 | } 118 | } else { 119 | // Should be valid against an empty schema 120 | $this->checkUndefined($v, new \stdClass(), $path, $k); 121 | } 122 | } 123 | } 124 | unset($v); /* remove dangling reference to prevent any future bugs 125 | * caused by accidentally using $v elsewhere */ 126 | 127 | // Treat when we have more schema definitions than values, not for empty arrays 128 | if (count($value) > 0) { 129 | for ($k = count($value); $k < count($schema->items); $k++) { 130 | $undefinedInstance = $this->factory->createInstanceFor('undefined'); 131 | $this->checkUndefined($undefinedInstance, $schema->items[$k], $path, $k); 132 | } 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/ConstConstraint.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class ConstConstraint extends Constraint 24 | { 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void 29 | { 30 | // Only validate const if the attribute exists 31 | if ($element instanceof UndefinedConstraint && (!isset($schema->required) || !$schema->required)) { 32 | return; 33 | } 34 | $const = $schema->const; 35 | 36 | $type = gettype($element); 37 | $constType = gettype($const); 38 | 39 | if ($this->factory->getConfig(self::CHECK_MODE_TYPE_CAST) && $type === 'array' && $constType === 'object') { 40 | if (DeepComparer::isEqual((object) $element, $const)) { 41 | return; 42 | } 43 | } 44 | 45 | if (DeepComparer::isEqual($element, $const)) { 46 | return; 47 | } 48 | 49 | $this->addError(ConstraintError::CONSTANT(), $path, ['const' => $schema->const]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/Constraint.php: -------------------------------------------------------------------------------- 1 | withPropertyPaths( 40 | array_merge( 41 | $path->getPropertyPaths(), 42 | [$i] 43 | ) 44 | ); 45 | } 46 | 47 | /** 48 | * Validates an array 49 | * 50 | * @param mixed $value 51 | * @param mixed $schema 52 | * @param mixed $i 53 | */ 54 | protected function checkArray(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void 55 | { 56 | $validator = $this->factory->createInstanceFor('collection'); 57 | $validator->check($value, $schema, $path, $i); 58 | 59 | $this->addErrors($validator->getErrors()); 60 | } 61 | 62 | /** 63 | * Validates an object 64 | * 65 | * @param mixed $value 66 | * @param mixed $schema 67 | * @param mixed $properties 68 | * @param mixed $additionalProperties 69 | * @param mixed $patternProperties 70 | * @param array $appliedDefaults 71 | */ 72 | protected function checkObject( 73 | &$value, 74 | $schema = null, 75 | ?JsonPointer $path = null, 76 | $properties = null, 77 | $additionalProperties = null, 78 | $patternProperties = null, 79 | array $appliedDefaults = [] 80 | ): void { 81 | /** @var ObjectConstraint $validator */ 82 | $validator = $this->factory->createInstanceFor('object'); 83 | $validator->check($value, $schema, $path, $properties, $additionalProperties, $patternProperties, $appliedDefaults); 84 | 85 | $this->addErrors($validator->getErrors()); 86 | } 87 | 88 | /** 89 | * Validates the type of the value 90 | * 91 | * @param mixed $value 92 | * @param mixed $schema 93 | * @param mixed $i 94 | */ 95 | protected function checkType(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void 96 | { 97 | $validator = $this->factory->createInstanceFor('type'); 98 | $validator->check($value, $schema, $path, $i); 99 | 100 | $this->addErrors($validator->getErrors()); 101 | } 102 | 103 | /** 104 | * Checks a undefined element 105 | * 106 | * @param mixed $value 107 | * @param mixed $schema 108 | * @param mixed $i 109 | */ 110 | protected function checkUndefined(&$value, $schema = null, ?JsonPointer $path = null, $i = null, bool $fromDefault = false): void 111 | { 112 | /** @var UndefinedConstraint $validator */ 113 | $validator = $this->factory->createInstanceFor('undefined'); 114 | 115 | $validator->check($value, $this->factory->getSchemaStorage()->resolveRefSchema($schema), $path, $i, $fromDefault); 116 | 117 | $this->addErrors($validator->getErrors()); 118 | } 119 | 120 | /** 121 | * Checks a string element 122 | * 123 | * @param mixed $value 124 | * @param mixed $schema 125 | * @param mixed $i 126 | */ 127 | protected function checkString($value, $schema = null, ?JsonPointer $path = null, $i = null): void 128 | { 129 | $validator = $this->factory->createInstanceFor('string'); 130 | $validator->check($value, $schema, $path, $i); 131 | 132 | $this->addErrors($validator->getErrors()); 133 | } 134 | 135 | /** 136 | * Checks a number element 137 | * 138 | * @param mixed $value 139 | * @param mixed $schema 140 | * @param mixed $i 141 | */ 142 | protected function checkNumber($value, $schema = null, ?JsonPointer $path = null, $i = null): void 143 | { 144 | $validator = $this->factory->createInstanceFor('number'); 145 | $validator->check($value, $schema, $path, $i); 146 | 147 | $this->addErrors($validator->getErrors()); 148 | } 149 | 150 | /** 151 | * Checks a enum element 152 | * 153 | * @param mixed $value 154 | * @param mixed $schema 155 | * @param mixed $i 156 | */ 157 | protected function checkEnum($value, $schema = null, ?JsonPointer $path = null, $i = null): void 158 | { 159 | $validator = $this->factory->createInstanceFor('enum'); 160 | $validator->check($value, $schema, $path, $i); 161 | 162 | $this->addErrors($validator->getErrors()); 163 | } 164 | 165 | /** 166 | * Checks a const element 167 | * 168 | * @param mixed $value 169 | * @param mixed $schema 170 | * @param mixed $i 171 | */ 172 | protected function checkConst($value, $schema = null, ?JsonPointer $path = null, $i = null): void 173 | { 174 | $validator = $this->factory->createInstanceFor('const'); 175 | $validator->check($value, $schema, $path, $i); 176 | 177 | $this->addErrors($validator->getErrors()); 178 | } 179 | 180 | /** 181 | * Checks format of an element 182 | * 183 | * @param mixed $value 184 | * @param mixed $schema 185 | * @param mixed $i 186 | */ 187 | protected function checkFormat($value, $schema = null, ?JsonPointer $path = null, $i = null): void 188 | { 189 | $validator = $this->factory->createInstanceFor('format'); 190 | $validator->check($value, $schema, $path, $i); 191 | 192 | $this->addErrors($validator->getErrors()); 193 | } 194 | 195 | /** 196 | * Get the type check based on the set check mode. 197 | */ 198 | protected function getTypeCheck(): TypeCheck\TypeCheckInterface 199 | { 200 | return $this->factory->getTypeCheck(); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/ConstraintInterface.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | interface ConstraintInterface 23 | { 24 | /** 25 | * returns all collected errors 26 | */ 27 | public function getErrors(): array; 28 | 29 | /** 30 | * adds errors to this validator 31 | */ 32 | public function addErrors(array $errors): void; 33 | 34 | /** 35 | * adds an error 36 | * 37 | * @param ConstraintError $constraint the constraint/rule that is broken, e.g.: ConstraintErrors::LENGTH_MIN() 38 | * @param array $more more array elements to add to the error 39 | */ 40 | public function addError(ConstraintError $constraint, ?JsonPointer $path = null, array $more = []): void; 41 | 42 | /** 43 | * checks if the validator has not raised errors 44 | */ 45 | public function isValid(): bool; 46 | 47 | /** 48 | * invokes the validation of an element 49 | * 50 | * @abstract 51 | * 52 | * @param mixed $value 53 | * @param mixed $schema 54 | * @param mixed $i 55 | * 56 | * @throws \JsonSchema\Exception\ExceptionInterface 57 | */ 58 | public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void; 59 | } 60 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/EnumConstraint.php: -------------------------------------------------------------------------------- 1 | 22 | * @author Bruno Prieto Reis 23 | */ 24 | class EnumConstraint extends Constraint 25 | { 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void 30 | { 31 | // Only validate enum if the attribute exists 32 | if ($element instanceof UndefinedConstraint && (!isset($schema->required) || !$schema->required)) { 33 | return; 34 | } 35 | $type = gettype($element); 36 | 37 | foreach ($schema->enum as $enum) { 38 | $enumType = gettype($enum); 39 | 40 | if ($enumType === 'object' 41 | && $type === 'array' 42 | && $this->factory->getConfig(self::CHECK_MODE_TYPE_CAST) 43 | && DeepComparer::isEqual((object) $element, $enum) 44 | ) { 45 | return; 46 | } 47 | 48 | if (($type === $enumType) && DeepComparer::isEqual($element, $enum)) { 49 | return; 50 | } 51 | 52 | if (is_numeric($element) && is_numeric($enum) && DeepComparer::isEqual((float) $element, (float) $enum)) { 53 | return; 54 | } 55 | } 56 | 57 | $this->addError(ConstraintError::ENUM(), $path, ['enum' => $schema->enum]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/Factory.php: -------------------------------------------------------------------------------- 1 | 39 | */ 40 | private $checkMode = Constraint::CHECK_MODE_NORMAL; 41 | 42 | /** 43 | * @var array 44 | * @phpstan-var array, TypeCheck\TypeCheckInterface> 45 | */ 46 | private $typeCheck = []; 47 | 48 | /** 49 | * @var int Validation context 50 | */ 51 | protected $errorContext = Validator::ERROR_DOCUMENT_VALIDATION; 52 | 53 | /** 54 | * @var array 55 | */ 56 | protected $constraintMap = [ 57 | 'array' => 'JsonSchema\Constraints\CollectionConstraint', 58 | 'collection' => 'JsonSchema\Constraints\CollectionConstraint', 59 | 'object' => 'JsonSchema\Constraints\ObjectConstraint', 60 | 'type' => 'JsonSchema\Constraints\TypeConstraint', 61 | 'undefined' => 'JsonSchema\Constraints\UndefinedConstraint', 62 | 'string' => 'JsonSchema\Constraints\StringConstraint', 63 | 'number' => 'JsonSchema\Constraints\NumberConstraint', 64 | 'enum' => 'JsonSchema\Constraints\EnumConstraint', 65 | 'const' => 'JsonSchema\Constraints\ConstConstraint', 66 | 'format' => 'JsonSchema\Constraints\FormatConstraint', 67 | 'schema' => 'JsonSchema\Constraints\SchemaConstraint', 68 | 'validator' => 'JsonSchema\Validator' 69 | ]; 70 | 71 | /** 72 | * @var array 73 | */ 74 | private $instanceCache = []; 75 | 76 | /** 77 | * @phpstan-param int-mask-of $checkMode 78 | */ 79 | public function __construct( 80 | ?SchemaStorageInterface $schemaStorage = null, 81 | ?UriRetrieverInterface $uriRetriever = null, 82 | int $checkMode = Constraint::CHECK_MODE_NORMAL 83 | ) { 84 | // set provided config options 85 | $this->setConfig($checkMode); 86 | 87 | $this->uriRetriever = $uriRetriever ?: new UriRetriever(); 88 | $this->schemaStorage = $schemaStorage ?: new SchemaStorage($this->uriRetriever); 89 | } 90 | 91 | /** 92 | * Set config values 93 | * 94 | * @param int $checkMode Set checkMode options - does not preserve existing flags 95 | * @phpstan-param int-mask-of $checkMode 96 | */ 97 | public function setConfig(int $checkMode = Constraint::CHECK_MODE_NORMAL): void 98 | { 99 | $this->checkMode = $checkMode; 100 | } 101 | 102 | /** 103 | * Enable checkMode flags 104 | * 105 | * @phpstan-param int-mask-of $options 106 | */ 107 | public function addConfig(int $options): void 108 | { 109 | $this->checkMode |= $options; 110 | } 111 | 112 | /** 113 | * Disable checkMode flags 114 | * 115 | * @phpstan-param int-mask-of $options 116 | */ 117 | public function removeConfig(int $options): void 118 | { 119 | $this->checkMode &= ~$options; 120 | } 121 | 122 | /** 123 | * Get checkMode option 124 | * 125 | * @param int|null $options Options to get, if null then return entire bitmask 126 | * @phpstan-param int-mask-of|null $options Options to get, if null then return entire bitmask 127 | * 128 | * @phpstan-return int-mask-of 129 | */ 130 | public function getConfig(?int $options = null): int 131 | { 132 | if ($options === null) { 133 | return $this->checkMode; 134 | } 135 | 136 | return $this->checkMode & $options; 137 | } 138 | 139 | public function getUriRetriever(): UriRetrieverInterface 140 | { 141 | return $this->uriRetriever; 142 | } 143 | 144 | public function getSchemaStorage(): SchemaStorageInterface 145 | { 146 | return $this->schemaStorage; 147 | } 148 | 149 | public function getTypeCheck(): TypeCheck\TypeCheckInterface 150 | { 151 | if (!isset($this->typeCheck[$this->checkMode])) { 152 | $this->typeCheck[$this->checkMode] = ($this->checkMode & Constraint::CHECK_MODE_TYPE_CAST) 153 | ? new TypeCheck\LooseTypeCheck() 154 | : new TypeCheck\StrictTypeCheck(); 155 | } 156 | 157 | return $this->typeCheck[$this->checkMode]; 158 | } 159 | 160 | public function setConstraintClass(string $name, string $class): Factory 161 | { 162 | // Ensure class exists 163 | if (!class_exists($class)) { 164 | throw new InvalidArgumentException('Unknown constraint ' . $name); 165 | } 166 | // Ensure class is appropriate 167 | if (!in_array('JsonSchema\Constraints\ConstraintInterface', class_implements($class))) { 168 | throw new InvalidArgumentException('Invalid class ' . $name); 169 | } 170 | $this->constraintMap[$name] = $class; 171 | 172 | return $this; 173 | } 174 | 175 | /** 176 | * Create a constraint instance for the given constraint name. 177 | * 178 | * @param string $constraintName 179 | * 180 | * @throws InvalidArgumentException if is not possible create the constraint instance 181 | * 182 | * @return ConstraintInterface&BaseConstraint 183 | * @phpstan-return ConstraintInterface&BaseConstraint 184 | */ 185 | public function createInstanceFor($constraintName) 186 | { 187 | if (!isset($this->constraintMap[$constraintName])) { 188 | throw new InvalidArgumentException('Unknown constraint ' . $constraintName); 189 | } 190 | 191 | if (!isset($this->instanceCache[$constraintName])) { 192 | $this->instanceCache[$constraintName] = new $this->constraintMap[$constraintName]($this); 193 | } 194 | 195 | return clone $this->instanceCache[$constraintName]; 196 | } 197 | 198 | /** 199 | * Get the error context 200 | * 201 | * @phpstan-return Validator::ERROR_DOCUMENT_VALIDATION|Validator::ERROR_SCHEMA_VALIDATION 202 | */ 203 | public function getErrorContext(): int 204 | { 205 | return $this->errorContext; 206 | } 207 | 208 | /** 209 | * Set the error context 210 | * 211 | * @phpstan-param Validator::ERROR_DOCUMENT_VALIDATION|Validator::ERROR_SCHEMA_VALIDATION $errorContext 212 | */ 213 | public function setErrorContext(int $errorContext): void 214 | { 215 | $this->errorContext = $errorContext; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/FormatConstraint.php: -------------------------------------------------------------------------------- 1 | 24 | * 25 | * @see http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.23 26 | */ 27 | class FormatConstraint extends Constraint 28 | { 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void 33 | { 34 | if (!isset($schema->format) || $this->factory->getConfig(self::CHECK_MODE_DISABLE_FORMAT)) { 35 | return; 36 | } 37 | 38 | switch ($schema->format) { 39 | case 'date': 40 | if (is_string($element) && !$date = $this->validateDateTime($element, 'Y-m-d')) { 41 | $this->addError(ConstraintError::FORMAT_DATE(), $path, [ 42 | 'date' => $element, 43 | 'format' => $schema->format 44 | ] 45 | ); 46 | } 47 | break; 48 | 49 | case 'time': 50 | if (is_string($element) && !$this->validateDateTime($element, 'H:i:s')) { 51 | $this->addError(ConstraintError::FORMAT_TIME(), $path, [ 52 | 'time' => json_encode($element), 53 | 'format' => $schema->format, 54 | ] 55 | ); 56 | } 57 | break; 58 | 59 | case 'date-time': 60 | if (is_string($element) && null === Rfc3339::createFromString($element)) { 61 | $this->addError(ConstraintError::FORMAT_DATE_TIME(), $path, [ 62 | 'dateTime' => json_encode($element), 63 | 'format' => $schema->format 64 | ] 65 | ); 66 | } 67 | break; 68 | 69 | case 'utc-millisec': 70 | if (!$this->validateDateTime($element, 'U')) { 71 | $this->addError(ConstraintError::FORMAT_DATE_UTC(), $path, [ 72 | 'value' => $element, 73 | 'format' => $schema->format]); 74 | } 75 | break; 76 | 77 | case 'regex': 78 | if (!$this->validateRegex($element)) { 79 | $this->addError(ConstraintError::FORMAT_REGEX(), $path, [ 80 | 'value' => $element, 81 | 'format' => $schema->format 82 | ] 83 | ); 84 | } 85 | break; 86 | 87 | case 'color': 88 | if (!$this->validateColor($element)) { 89 | $this->addError(ConstraintError::FORMAT_COLOR(), $path, ['format' => $schema->format]); 90 | } 91 | break; 92 | 93 | case 'style': 94 | if (!$this->validateStyle($element)) { 95 | $this->addError(ConstraintError::FORMAT_STYLE(), $path, ['format' => $schema->format]); 96 | } 97 | break; 98 | 99 | case 'phone': 100 | if (!$this->validatePhone($element)) { 101 | $this->addError(ConstraintError::FORMAT_PHONE(), $path, ['format' => $schema->format]); 102 | } 103 | break; 104 | 105 | case 'uri': 106 | if (is_string($element) && !UriValidator::isValid($element)) { 107 | $this->addError(ConstraintError::FORMAT_URL(), $path, ['format' => $schema->format]); 108 | } 109 | break; 110 | 111 | case 'uriref': 112 | case 'uri-reference': 113 | if (is_string($element) && !(UriValidator::isValid($element) || RelativeReferenceValidator::isValid($element))) { 114 | $this->addError(ConstraintError::FORMAT_URL(), $path, ['format' => $schema->format]); 115 | } 116 | break; 117 | 118 | case 'email': 119 | if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE | FILTER_FLAG_EMAIL_UNICODE)) { 120 | $this->addError(ConstraintError::FORMAT_EMAIL(), $path, ['format' => $schema->format]); 121 | } 122 | break; 123 | 124 | case 'ip-address': 125 | case 'ipv4': 126 | if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4)) { 127 | $this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]); 128 | } 129 | break; 130 | 131 | case 'ipv6': 132 | if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV6)) { 133 | $this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]); 134 | } 135 | break; 136 | 137 | case 'host-name': 138 | case 'hostname': 139 | if (!$this->validateHostname($element)) { 140 | $this->addError(ConstraintError::FORMAT_HOSTNAME(), $path, ['format' => $schema->format]); 141 | } 142 | break; 143 | 144 | default: 145 | // Empty as it should be: 146 | // The value of this keyword is called a format attribute. It MUST be a string. 147 | // A format attribute can generally only validate a given set of instance types. 148 | // If the type of the instance to validate is not in this set, validation for 149 | // this format attribute and instance SHOULD succeed. 150 | // http://json-schema.org/latest/json-schema-validation.html#anchor105 151 | break; 152 | } 153 | } 154 | 155 | protected function validateDateTime($datetime, $format) 156 | { 157 | $dt = \DateTime::createFromFormat($format, (string) $datetime); 158 | 159 | if (!$dt) { 160 | return false; 161 | } 162 | 163 | if ($datetime === $dt->format($format)) { 164 | return true; 165 | } 166 | 167 | return false; 168 | } 169 | 170 | protected function validateRegex($regex) 171 | { 172 | if (!is_string($regex)) { 173 | return true; 174 | } 175 | 176 | return false !== @preg_match(self::jsonPatternToPhpRegex($regex), ''); 177 | } 178 | 179 | protected function validateColor($color) 180 | { 181 | if (!is_string($color)) { 182 | return true; 183 | } 184 | 185 | if (in_array(strtolower($color), ['aqua', 'black', 'blue', 'fuchsia', 186 | 'gray', 'green', 'lime', 'maroon', 'navy', 'olive', 'orange', 'purple', 187 | 'red', 'silver', 'teal', 'white', 'yellow'])) { 188 | return true; 189 | } 190 | 191 | return preg_match('/^#([a-f0-9]{3}|[a-f0-9]{6})$/i', $color); 192 | } 193 | 194 | protected function validateStyle($style) 195 | { 196 | $properties = explode(';', rtrim($style, ';')); 197 | $invalidEntries = preg_grep('/^\s*[-a-z]+\s*:\s*.+$/i', $properties, PREG_GREP_INVERT); 198 | 199 | return empty($invalidEntries); 200 | } 201 | 202 | protected function validatePhone($phone) 203 | { 204 | return preg_match('/^\+?(\(\d{3}\)|\d{3}) \d{3} \d{4}$/', $phone); 205 | } 206 | 207 | protected function validateHostname($host) 208 | { 209 | if (!is_string($host)) { 210 | return true; 211 | } 212 | 213 | $hostnameRegex = '/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/i'; 214 | 215 | return preg_match($hostnameRegex, $host); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/NumberConstraint.php: -------------------------------------------------------------------------------- 1 | 21 | * @author Bruno Prieto Reis 22 | */ 23 | class NumberConstraint extends Constraint 24 | { 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void 29 | { 30 | // Verify minimum 31 | if (isset($schema->exclusiveMinimum)) { 32 | if (isset($schema->minimum)) { 33 | if ($schema->exclusiveMinimum && $element <= $schema->minimum) { 34 | $this->addError(ConstraintError::EXCLUSIVE_MINIMUM(), $path, ['minimum' => $schema->minimum]); 35 | } elseif ($element < $schema->minimum) { 36 | $this->addError(ConstraintError::MINIMUM(), $path, ['minimum' => $schema->minimum]); 37 | } 38 | } else { 39 | $this->addError(ConstraintError::MISSING_MINIMUM(), $path); 40 | } 41 | } elseif (isset($schema->minimum) && $element < $schema->minimum) { 42 | $this->addError(ConstraintError::MINIMUM(), $path, ['minimum' => $schema->minimum]); 43 | } 44 | 45 | // Verify maximum 46 | if (isset($schema->exclusiveMaximum)) { 47 | if (isset($schema->maximum)) { 48 | if ($schema->exclusiveMaximum && $element >= $schema->maximum) { 49 | $this->addError(ConstraintError::EXCLUSIVE_MAXIMUM(), $path, ['maximum' => $schema->maximum]); 50 | } elseif ($element > $schema->maximum) { 51 | $this->addError(ConstraintError::MAXIMUM(), $path, ['maximum' => $schema->maximum]); 52 | } 53 | } else { 54 | $this->addError(ConstraintError::MISSING_MAXIMUM(), $path); 55 | } 56 | } elseif (isset($schema->maximum) && $element > $schema->maximum) { 57 | $this->addError(ConstraintError::MAXIMUM(), $path, ['maximum' => $schema->maximum]); 58 | } 59 | 60 | // Verify divisibleBy - Draft v3 61 | if (isset($schema->divisibleBy) && $this->fmod($element, $schema->divisibleBy) != 0) { 62 | $this->addError(ConstraintError::DIVISIBLE_BY(), $path, ['divisibleBy' => $schema->divisibleBy]); 63 | } 64 | 65 | // Verify multipleOf - Draft v4 66 | if (isset($schema->multipleOf) && $this->fmod($element, $schema->multipleOf) != 0) { 67 | $this->addError(ConstraintError::MULTIPLE_OF(), $path, ['multipleOf' => $schema->multipleOf]); 68 | } 69 | 70 | $this->checkFormat($element, $schema, $path, $i); 71 | } 72 | 73 | private function fmod($number1, $number2) 74 | { 75 | $modulus = ($number1 - round($number1 / $number2) * $number2); 76 | $precision = 0.0000000001; 77 | 78 | if (-$precision < $modulus && $modulus < $precision) { 79 | return 0.0; 80 | } 81 | 82 | return $modulus; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/ObjectConstraint.php: -------------------------------------------------------------------------------- 1 | List of properties to which a default value has been applied 14 | */ 15 | protected $appliedDefaults = []; 16 | 17 | /** 18 | * {@inheritdoc} 19 | * 20 | * @param list $appliedDefaults 21 | */ 22 | public function check( 23 | &$element, 24 | $schema = null, 25 | ?JsonPointer $path = null, 26 | $properties = null, 27 | $additionalProp = null, 28 | $patternProperties = null, 29 | $appliedDefaults = [] 30 | ): void { 31 | if ($element instanceof UndefinedConstraint) { 32 | return; 33 | } 34 | 35 | $this->appliedDefaults = $appliedDefaults; 36 | 37 | $matches = []; 38 | if ($patternProperties) { 39 | // validate the element pattern properties 40 | $matches = $this->validatePatternProperties($element, $path, $patternProperties); 41 | } 42 | 43 | if ($properties) { 44 | // validate the element properties 45 | $this->validateProperties($element, $properties, $path); 46 | } 47 | 48 | // validate additional element properties & constraints 49 | $this->validateElement($element, $matches, $schema, $path, $properties, $additionalProp); 50 | } 51 | 52 | public function validatePatternProperties($element, ?JsonPointer $path, $patternProperties) 53 | { 54 | $matches = []; 55 | foreach ($patternProperties as $pregex => $schema) { 56 | $fullRegex = self::jsonPatternToPhpRegex($pregex); 57 | 58 | // Validate the pattern before using it to test for matches 59 | if (@preg_match($fullRegex, '') === false) { 60 | $this->addError(ConstraintError::PREGEX_INVALID(), $path, ['pregex' => $pregex]); 61 | continue; 62 | } 63 | foreach ($element as $i => $value) { 64 | if (preg_match($fullRegex, $i)) { 65 | $matches[] = $i; 66 | $this->checkUndefined($value, $schema ?: new \stdClass(), $path, $i, in_array($i, $this->appliedDefaults)); 67 | } 68 | } 69 | } 70 | 71 | return $matches; 72 | } 73 | 74 | /** 75 | * Validates the element properties 76 | * 77 | * @param \StdClass $element Element to validate 78 | * @param array $matches Matches from patternProperties (if any) 79 | * @param \StdClass $schema ObjectConstraint definition 80 | * @param JsonPointer|null $path Current test path 81 | * @param \StdClass $properties Properties 82 | * @param mixed $additionalProp Additional properties 83 | */ 84 | public function validateElement($element, $matches, $schema = null, ?JsonPointer $path = null, 85 | $properties = null, $additionalProp = null) 86 | { 87 | $this->validateMinMaxConstraint($element, $schema, $path); 88 | 89 | foreach ($element as $i => $value) { 90 | $definition = $this->getProperty($properties, $i); 91 | 92 | // no additional properties allowed 93 | if (!in_array($i, $matches) && $additionalProp === false && $this->inlineSchemaProperty !== $i && !$definition) { 94 | $this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), $path, ['property' => $i]); 95 | } 96 | 97 | // additional properties defined 98 | if (!in_array($i, $matches) && $additionalProp && !$definition) { 99 | if ($additionalProp === true) { 100 | $this->checkUndefined($value, null, $path, $i, in_array($i, $this->appliedDefaults)); 101 | } else { 102 | $this->checkUndefined($value, $additionalProp, $path, $i, in_array($i, $this->appliedDefaults)); 103 | } 104 | } 105 | 106 | // property requires presence of another 107 | $require = $this->getProperty($definition, 'requires'); 108 | if ($require && !$this->getProperty($element, $require)) { 109 | $this->addError(ConstraintError::REQUIRES(), $path, [ 110 | 'property' => $i, 111 | 'requiredProperty' => $require 112 | ]); 113 | } 114 | 115 | $property = $this->getProperty($element, $i, $this->factory->createInstanceFor('undefined')); 116 | if (is_object($property)) { 117 | $this->validateMinMaxConstraint(!($property instanceof UndefinedConstraint) ? $property : $element, $definition, $path); 118 | } 119 | } 120 | } 121 | 122 | /** 123 | * Validates the definition properties 124 | * 125 | * @param \stdClass $element Element to validate 126 | * @param \stdClass $properties Property definitions 127 | * @param JsonPointer|null $path Path? 128 | */ 129 | public function validateProperties(&$element, $properties = null, ?JsonPointer $path = null) 130 | { 131 | $undefinedConstraint = $this->factory->createInstanceFor('undefined'); 132 | 133 | foreach ($properties as $i => $value) { 134 | $property = &$this->getProperty($element, $i, $undefinedConstraint); 135 | $definition = $this->getProperty($properties, $i); 136 | 137 | if (is_object($definition)) { 138 | // Undefined constraint will check for is_object() and quit if is not - so why pass it? 139 | $this->checkUndefined($property, $definition, $path, $i, in_array($i, $this->appliedDefaults)); 140 | } 141 | } 142 | } 143 | 144 | /** 145 | * retrieves a property from an object or array 146 | * 147 | * @param mixed $element Element to validate 148 | * @param string $property Property to retrieve 149 | * @param mixed $fallback Default value if property is not found 150 | * 151 | * @return mixed 152 | */ 153 | protected function &getProperty(&$element, $property, $fallback = null) 154 | { 155 | if (is_array($element) && (isset($element[$property]) || array_key_exists($property, $element)) /*$this->checkMode == self::CHECK_MODE_TYPE_CAST*/) { 156 | return $element[$property]; 157 | } elseif (is_object($element) && property_exists($element, (string) $property)) { 158 | return $element->$property; 159 | } 160 | 161 | return $fallback; 162 | } 163 | 164 | /** 165 | * validating minimum and maximum property constraints (if present) against an element 166 | * 167 | * @param \stdClass $element Element to validate 168 | * @param \stdClass $objectDefinition ObjectConstraint definition 169 | * @param JsonPointer|null $path Path to test? 170 | */ 171 | protected function validateMinMaxConstraint($element, $objectDefinition, ?JsonPointer $path = null) 172 | { 173 | if (!$this->getTypeCheck()::isObject($element)) { 174 | return; 175 | } 176 | 177 | // Verify minimum number of properties 178 | if (isset($objectDefinition->minProperties) && is_int($objectDefinition->minProperties)) { 179 | if ($this->getTypeCheck()->propertyCount($element) < max(0, $objectDefinition->minProperties)) { 180 | $this->addError(ConstraintError::PROPERTIES_MIN(), $path, ['minProperties' => $objectDefinition->minProperties]); 181 | } 182 | } 183 | // Verify maximum number of properties 184 | if (isset($objectDefinition->maxProperties) && is_int($objectDefinition->maxProperties)) { 185 | if ($this->getTypeCheck()->propertyCount($element) > max(0, $objectDefinition->maxProperties)) { 186 | $this->addError(ConstraintError::PROPERTIES_MAX(), $path, ['maxProperties' => $objectDefinition->maxProperties]); 187 | } 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/SchemaConstraint.php: -------------------------------------------------------------------------------- 1 | 25 | * @author Bruno Prieto Reis 26 | */ 27 | class SchemaConstraint extends Constraint 28 | { 29 | private const DEFAULT_SCHEMA_SPEC = 'http://json-schema.org/draft-04/schema#'; 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void 35 | { 36 | if ($schema !== null) { 37 | // passed schema 38 | $validationSchema = $schema; 39 | } elseif ($this->getTypeCheck()->propertyExists($element, $this->inlineSchemaProperty)) { 40 | // inline schema 41 | $validationSchema = $this->getTypeCheck()->propertyGet($element, $this->inlineSchemaProperty); 42 | } else { 43 | throw new InvalidArgumentException('no schema found to verify against'); 44 | } 45 | 46 | // cast array schemas to object 47 | if (is_array($validationSchema)) { 48 | $validationSchema = BaseConstraint::arrayToObjectRecursive($validationSchema); 49 | } 50 | 51 | // validate schema against whatever is defined in $validationSchema->$schema. If no 52 | // schema is defined, assume self::DEFAULT_SCHEMA_SPEC (currently draft-04). 53 | if ($this->factory->getConfig(self::CHECK_MODE_VALIDATE_SCHEMA)) { 54 | if (!$this->getTypeCheck()->isObject($validationSchema)) { 55 | throw new RuntimeException('Cannot validate the schema of a non-object'); 56 | } 57 | if ($this->getTypeCheck()->propertyExists($validationSchema, '$schema')) { 58 | $schemaSpec = $this->getTypeCheck()->propertyGet($validationSchema, '$schema'); 59 | } else { 60 | $schemaSpec = self::DEFAULT_SCHEMA_SPEC; 61 | } 62 | 63 | // get the spec schema 64 | $schemaStorage = $this->factory->getSchemaStorage(); 65 | if (!$this->getTypeCheck()->isObject($schemaSpec)) { 66 | $schemaSpec = $schemaStorage->getSchema($schemaSpec); 67 | } 68 | 69 | // save error count, config & subtract CHECK_MODE_VALIDATE_SCHEMA 70 | $initialErrorCount = $this->numErrors(); 71 | $initialConfig = $this->factory->getConfig(); 72 | $initialContext = $this->factory->getErrorContext(); 73 | $this->factory->removeConfig(self::CHECK_MODE_VALIDATE_SCHEMA | self::CHECK_MODE_APPLY_DEFAULTS); 74 | $this->factory->addConfig(self::CHECK_MODE_TYPE_CAST); 75 | $this->factory->setErrorContext(Validator::ERROR_SCHEMA_VALIDATION); 76 | 77 | // validate schema 78 | try { 79 | $this->check($validationSchema, $schemaSpec); 80 | } catch (\Exception $e) { 81 | if ($this->factory->getConfig(self::CHECK_MODE_EXCEPTIONS)) { 82 | throw new InvalidSchemaException('Schema did not pass validation', 0, $e); 83 | } 84 | } 85 | if ($this->numErrors() > $initialErrorCount) { 86 | $this->addError(ConstraintError::INVALID_SCHEMA(), $path); 87 | } 88 | 89 | // restore the initial config 90 | $this->factory->setConfig($initialConfig); 91 | $this->factory->setErrorContext($initialContext); 92 | } 93 | 94 | // validate element against $validationSchema 95 | $this->checkUndefined($element, $validationSchema, $path, $i); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/StringConstraint.php: -------------------------------------------------------------------------------- 1 | 21 | * @author Bruno Prieto Reis 22 | */ 23 | class StringConstraint extends Constraint 24 | { 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void 29 | { 30 | // Verify maxLength 31 | if (isset($schema->maxLength) && $this->strlen($element) > $schema->maxLength) { 32 | $this->addError(ConstraintError::LENGTH_MAX(), $path, [ 33 | 'maxLength' => $schema->maxLength, 34 | ]); 35 | } 36 | 37 | //verify minLength 38 | if (isset($schema->minLength) && $this->strlen($element) < $schema->minLength) { 39 | $this->addError(ConstraintError::LENGTH_MIN(), $path, [ 40 | 'minLength' => $schema->minLength, 41 | ]); 42 | } 43 | 44 | // Verify a regex pattern 45 | if (isset($schema->pattern) && !preg_match(self::jsonPatternToPhpRegex($schema->pattern), $element)) { 46 | $this->addError(ConstraintError::PATTERN(), $path, [ 47 | 'pattern' => $schema->pattern, 48 | ]); 49 | } 50 | 51 | $this->checkFormat($element, $schema, $path, $i); 52 | } 53 | 54 | private function strlen($string) 55 | { 56 | if (extension_loaded('mbstring')) { 57 | return mb_strlen($string, mb_detect_encoding($string)); 58 | } 59 | 60 | // mbstring is present on all test platforms, so strlen() can be ignored for coverage 61 | return strlen($string); // @codeCoverageIgnore 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php: -------------------------------------------------------------------------------- 1 | {$property}; 27 | } 28 | 29 | return $value[$property]; 30 | } 31 | 32 | public static function propertySet(&$value, $property, $data) 33 | { 34 | if (is_object($value)) { 35 | $value->{$property} = $data; 36 | } else { 37 | $value[$property] = $data; 38 | } 39 | } 40 | 41 | public static function propertyExists($value, $property) 42 | { 43 | if (is_object($value)) { 44 | return property_exists($value, $property); 45 | } 46 | 47 | return is_array($value) && array_key_exists($property, $value); 48 | } 49 | 50 | public static function propertyCount($value) 51 | { 52 | if (is_object($value)) { 53 | return count(get_object_vars($value)); 54 | } 55 | 56 | return count($value); 57 | } 58 | 59 | /** 60 | * Check if the provided array is associative or not 61 | * 62 | * @param array $arr 63 | * 64 | * @return bool 65 | */ 66 | private static function isAssociativeArray($arr) 67 | { 68 | return array_keys($arr) !== range(0, count($arr) - 1); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php: -------------------------------------------------------------------------------- 1 | {$property}; 22 | } 23 | 24 | public static function propertySet(&$value, $property, $data) 25 | { 26 | $value->{$property} = $data; 27 | } 28 | 29 | public static function propertyExists($value, $property) 30 | { 31 | return property_exists($value, $property); 32 | } 33 | 34 | public static function propertyCount($value) 35 | { 36 | if (!is_object($value)) { 37 | return 0; 38 | } 39 | 40 | return count(get_object_vars($value)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php: -------------------------------------------------------------------------------- 1 | 23 | * @author Bruno Prieto Reis 24 | */ 25 | class TypeConstraint extends Constraint 26 | { 27 | /** 28 | * @var array|string[] type wordings for validation error messages 29 | */ 30 | public static $wording = [ 31 | 'integer' => 'an integer', 32 | 'number' => 'a number', 33 | 'boolean' => 'a boolean', 34 | 'object' => 'an object', 35 | 'array' => 'an array', 36 | 'string' => 'a string', 37 | 'null' => 'a null', 38 | 'any' => null, // validation of 'any' is always true so is not needed in message wording 39 | 0 => null, // validation of a false-y value is always true, so not needed as well 40 | ]; 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function check(&$value = null, $schema = null, ?JsonPointer $path = null, $i = null): void 46 | { 47 | $type = isset($schema->type) ? $schema->type : null; 48 | $isValid = false; 49 | $coerce = $this->factory->getConfig(self::CHECK_MODE_COERCE_TYPES); 50 | $earlyCoerce = $this->factory->getConfig(self::CHECK_MODE_EARLY_COERCE); 51 | $wording = []; 52 | 53 | if (is_array($type)) { 54 | $this->validateTypesArray($value, $type, $wording, $isValid, $path, $coerce && $earlyCoerce); 55 | if (!$isValid && $coerce && !$earlyCoerce) { 56 | $this->validateTypesArray($value, $type, $wording, $isValid, $path, true); 57 | } 58 | } elseif (is_object($type)) { 59 | $this->checkUndefined($value, $type, $path); 60 | 61 | return; 62 | } else { 63 | $isValid = $this->validateType($value, $type, $coerce && $earlyCoerce); 64 | if (!$isValid && $coerce && !$earlyCoerce) { 65 | $isValid = $this->validateType($value, $type, true); 66 | } 67 | } 68 | 69 | if ($isValid === false) { 70 | if (!is_array($type)) { 71 | $this->validateTypeNameWording($type); 72 | $wording[] = self::$wording[$type]; 73 | } 74 | $this->addError(ConstraintError::TYPE(), $path, [ 75 | 'found' => gettype($value), 76 | 'expected' => $this->implodeWith($wording, ', ', 'or') 77 | ]); 78 | } 79 | } 80 | 81 | /** 82 | * Validates the given $value against the array of types in $type. Sets the value 83 | * of $isValid to true, if at least one $type mateches the type of $value or the value 84 | * passed as $isValid is already true. 85 | * 86 | * @param mixed $value Value to validate 87 | * @param array $type TypeConstraints to check against 88 | * @param array $validTypesWording An array of wordings of the valid types of the array $type 89 | * @param bool $isValid The current validation value 90 | * @param ?JsonPointer $path 91 | * @param bool $coerce 92 | */ 93 | protected function validateTypesArray(&$value, array $type, &$validTypesWording, &$isValid, $path, $coerce = false) 94 | { 95 | foreach ($type as $tp) { 96 | // already valid, so no need to waste cycles looping over everything 97 | if ($isValid) { 98 | return; 99 | } 100 | 101 | // $tp can be an object, if it's a schema instead of a simple type, validate it 102 | // with a new type constraint 103 | if (is_object($tp)) { 104 | if (!$isValid) { 105 | $validator = $this->factory->createInstanceFor('type'); 106 | $subSchema = new \stdClass(); 107 | $subSchema->type = $tp; 108 | $validator->check($value, $subSchema, $path, null); 109 | $error = $validator->getErrors(); 110 | $isValid = !(bool) $error; 111 | $validTypesWording[] = self::$wording['object']; 112 | } 113 | } else { 114 | $this->validateTypeNameWording($tp); 115 | $validTypesWording[] = self::$wording[$tp]; 116 | if (!$isValid) { 117 | $isValid = $this->validateType($value, $tp, $coerce); 118 | } 119 | } 120 | } 121 | } 122 | 123 | /** 124 | * Implodes the given array like implode() with turned around parameters and with the 125 | * difference, that, if $listEnd isn't false, the last element delimiter is $listEnd instead of 126 | * $delimiter. 127 | * 128 | * @param array $elements The elements to implode 129 | * @param string $delimiter The delimiter to use 130 | * @param bool $listEnd The last delimiter to use (defaults to $delimiter) 131 | * 132 | * @return string 133 | */ 134 | protected function implodeWith(array $elements, $delimiter = ', ', $listEnd = false) 135 | { 136 | if ($listEnd === false || !isset($elements[1])) { 137 | return implode($delimiter, $elements); 138 | } 139 | $lastElement = array_slice($elements, -1); 140 | $firsElements = join($delimiter, array_slice($elements, 0, -1)); 141 | $implodedElements = array_merge([$firsElements], $lastElement); 142 | 143 | return join(" $listEnd ", $implodedElements); 144 | } 145 | 146 | /** 147 | * Validates the given $type, if there's an associated self::$wording. If not, throws an 148 | * exception. 149 | * 150 | * @param string $type The type to validate 151 | * 152 | * @throws StandardUnexpectedValueException 153 | */ 154 | protected function validateTypeNameWording($type) 155 | { 156 | if (!array_key_exists($type, self::$wording)) { 157 | throw new StandardUnexpectedValueException( 158 | sprintf( 159 | 'No wording for %s available, expected wordings are: [%s]', 160 | var_export($type, true), 161 | implode(', ', array_filter(self::$wording))) 162 | ); 163 | } 164 | } 165 | 166 | /** 167 | * Verifies that a given value is of a certain type 168 | * 169 | * @param mixed $value Value to validate 170 | * @param string $type TypeConstraint to check against 171 | * 172 | * @throws InvalidArgumentException 173 | * 174 | * @return bool 175 | */ 176 | protected function validateType(&$value, $type, $coerce = false) 177 | { 178 | //mostly the case for inline schema 179 | if (!$type) { 180 | return true; 181 | } 182 | 183 | if ('any' === $type) { 184 | return true; 185 | } 186 | 187 | if ('object' === $type) { 188 | return $this->getTypeCheck()->isObject($value); 189 | } 190 | 191 | if ('array' === $type) { 192 | if ($coerce) { 193 | $value = $this->toArray($value); 194 | } 195 | 196 | return $this->getTypeCheck()->isArray($value); 197 | } 198 | 199 | if ('integer' === $type) { 200 | if ($coerce) { 201 | $value = $this->toInteger($value); 202 | } 203 | 204 | return is_int($value); 205 | } 206 | 207 | if ('number' === $type) { 208 | if ($coerce) { 209 | $value = $this->toNumber($value); 210 | } 211 | 212 | return is_numeric($value) && !is_string($value); 213 | } 214 | 215 | if ('boolean' === $type) { 216 | if ($coerce) { 217 | $value = $this->toBoolean($value); 218 | } 219 | 220 | return is_bool($value); 221 | } 222 | 223 | if ('string' === $type) { 224 | if ($coerce) { 225 | $value = $this->toString($value); 226 | } 227 | 228 | return is_string($value); 229 | } 230 | 231 | if ('null' === $type) { 232 | if ($coerce) { 233 | $value = $this->toNull($value); 234 | } 235 | 236 | return is_null($value); 237 | } 238 | 239 | throw new InvalidArgumentException((is_object($value) ? 'object' : $value) . ' is an invalid type for ' . $type); 240 | } 241 | 242 | /** 243 | * Converts a value to boolean. For example, "true" becomes true. 244 | * 245 | * @param mixed $value The value to convert to boolean 246 | * 247 | * @return bool|mixed 248 | */ 249 | protected function toBoolean($value) 250 | { 251 | if ($value === 1 || $value === 'true') { 252 | return true; 253 | } 254 | if (is_null($value) || $value === 0 || $value === 'false') { 255 | return false; 256 | } 257 | if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { 258 | return $this->toBoolean(reset($value)); 259 | } 260 | 261 | return $value; 262 | } 263 | 264 | /** 265 | * Converts a value to a number. For example, "4.5" becomes 4.5. 266 | * 267 | * @param mixed $value the value to convert to a number 268 | * 269 | * @return int|float|mixed 270 | */ 271 | protected function toNumber($value) 272 | { 273 | if (is_numeric($value)) { 274 | return $value + 0; // cast to number 275 | } 276 | if (is_bool($value) || is_null($value)) { 277 | return (int) $value; 278 | } 279 | if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { 280 | return $this->toNumber(reset($value)); 281 | } 282 | 283 | return $value; 284 | } 285 | 286 | /** 287 | * Converts a value to an integer. For example, "4" becomes 4. 288 | * 289 | * @param mixed $value 290 | * 291 | * @return int|mixed 292 | */ 293 | protected function toInteger($value) 294 | { 295 | $numberValue = $this->toNumber($value); 296 | if (is_numeric($numberValue) && (int) $numberValue == $numberValue) { 297 | return (int) $numberValue; // cast to number 298 | } 299 | 300 | return $value; 301 | } 302 | 303 | /** 304 | * Converts a value to an array containing that value. For example, [4] becomes 4. 305 | * 306 | * @param mixed $value 307 | * 308 | * @return array|mixed 309 | */ 310 | protected function toArray($value) 311 | { 312 | if (is_scalar($value) || is_null($value)) { 313 | return [$value]; 314 | } 315 | 316 | return $value; 317 | } 318 | 319 | /** 320 | * Convert a value to a string representation of that value. For example, null becomes "". 321 | * 322 | * @param mixed $value 323 | * 324 | * @return string|mixed 325 | */ 326 | protected function toString($value) 327 | { 328 | if (is_numeric($value)) { 329 | return "$value"; 330 | } 331 | if ($value === true) { 332 | return 'true'; 333 | } 334 | if ($value === false) { 335 | return 'false'; 336 | } 337 | if (is_null($value)) { 338 | return ''; 339 | } 340 | if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { 341 | return $this->toString(reset($value)); 342 | } 343 | 344 | return $value; 345 | } 346 | 347 | /** 348 | * Convert a value to a null. For example, 0 becomes null. 349 | * 350 | * @param mixed $value 351 | * 352 | * @return null|mixed 353 | */ 354 | protected function toNull($value) 355 | { 356 | if ($value === 0 || $value === false || $value === '') { 357 | return null; 358 | } 359 | if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { 360 | return $this->toNull(reset($value)); 361 | } 362 | 363 | return $value; 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /src/JsonSchema/Constraints/UndefinedConstraint.php: -------------------------------------------------------------------------------- 1 | List of properties to which a default value has been applied 19 | */ 20 | protected $appliedDefaults = []; 21 | 22 | /** 23 | * {@inheritdoc} 24 | * */ 25 | public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null, bool $fromDefault = false): void 26 | { 27 | if (is_null($schema) || !is_object($schema)) { 28 | return; 29 | } 30 | 31 | $path = $this->incrementPath($path, $i); 32 | if ($fromDefault) { 33 | $path->setFromDefault(); 34 | } 35 | 36 | // check special properties 37 | $this->validateCommonProperties($value, $schema, $path, $i); 38 | 39 | // check allOf, anyOf, and oneOf properties 40 | $this->validateOfProperties($value, $schema, $path, ''); 41 | 42 | // check known types 43 | $this->validateTypes($value, $schema, $path, $i); 44 | } 45 | 46 | /** 47 | * Validates the value against the types 48 | * 49 | * @param mixed $value 50 | * @param mixed $schema 51 | * @param JsonPointer $path 52 | * @param string $i 53 | */ 54 | public function validateTypes(&$value, $schema, JsonPointer $path, $i = null) 55 | { 56 | // check array 57 | if ($this->getTypeCheck()->isArray($value)) { 58 | $this->checkArray($value, $schema, $path, $i); 59 | } 60 | 61 | // check object 62 | if (LooseTypeCheck::isObject($value)) { 63 | // object processing should always be run on assoc arrays, 64 | // so use LooseTypeCheck here even if CHECK_MODE_TYPE_CAST 65 | // is not set (i.e. don't use $this->getTypeCheck() here). 66 | $this->checkObject( 67 | $value, 68 | $schema, 69 | $path, 70 | isset($schema->properties) ? $schema->properties : null, 71 | isset($schema->additionalProperties) ? $schema->additionalProperties : null, 72 | isset($schema->patternProperties) ? $schema->patternProperties : null, 73 | $this->appliedDefaults 74 | ); 75 | } 76 | 77 | // check string 78 | if (is_string($value)) { 79 | $this->checkString($value, $schema, $path, $i); 80 | } 81 | 82 | // check numeric 83 | if (is_numeric($value)) { 84 | $this->checkNumber($value, $schema, $path, $i); 85 | } 86 | 87 | // check enum 88 | if (isset($schema->enum)) { 89 | $this->checkEnum($value, $schema, $path, $i); 90 | } 91 | 92 | // check const 93 | if (isset($schema->const)) { 94 | $this->checkConst($value, $schema, $path, $i); 95 | } 96 | } 97 | 98 | /** 99 | * Validates common properties 100 | * 101 | * @param mixed $value 102 | * @param mixed $schema 103 | * @param JsonPointer $path 104 | * @param string $i 105 | */ 106 | protected function validateCommonProperties(&$value, $schema, JsonPointer $path, $i = '') 107 | { 108 | // if it extends another schema, it must pass that schema as well 109 | if (isset($schema->extends)) { 110 | if (is_string($schema->extends)) { 111 | $schema->extends = $this->validateUri($schema, $schema->extends); 112 | } 113 | if (is_array($schema->extends)) { 114 | foreach ($schema->extends as $extends) { 115 | $this->checkUndefined($value, $extends, $path, $i); 116 | } 117 | } else { 118 | $this->checkUndefined($value, $schema->extends, $path, $i); 119 | } 120 | } 121 | 122 | // Apply default values from schema 123 | if (!$path->fromDefault()) { 124 | $this->applyDefaultValues($value, $schema, $path); 125 | } 126 | 127 | // Verify required values 128 | if ($this->getTypeCheck()->isObject($value)) { 129 | if (!($value instanceof self) && isset($schema->required) && is_array($schema->required)) { 130 | // Draft 4 - Required is an array of strings - e.g. "required": ["foo", ...] 131 | foreach ($schema->required as $required) { 132 | if (!$this->getTypeCheck()->propertyExists($value, $required)) { 133 | $this->addError( 134 | ConstraintError::REQUIRED(), 135 | $this->incrementPath($path, $required), [ 136 | 'property' => $required 137 | ] 138 | ); 139 | } 140 | } 141 | } elseif (isset($schema->required) && !is_array($schema->required)) { 142 | // Draft 3 - Required attribute - e.g. "foo": {"type": "string", "required": true} 143 | if ($schema->required && $value instanceof self) { 144 | $propertyPaths = $path->getPropertyPaths(); 145 | $propertyName = end($propertyPaths); 146 | $this->addError(ConstraintError::REQUIRED(), $path, ['property' => $propertyName]); 147 | } 148 | } else { 149 | // if the value is both undefined and not required, skip remaining checks 150 | // in this method which assume an actual, defined instance when validating. 151 | if ($value instanceof self) { 152 | return; 153 | } 154 | } 155 | } 156 | 157 | // Verify type 158 | if (!($value instanceof self)) { 159 | $this->checkType($value, $schema, $path, $i); 160 | } 161 | 162 | // Verify disallowed items 163 | if (isset($schema->disallow)) { 164 | $initErrors = $this->getErrors(); 165 | 166 | $typeSchema = new \stdClass(); 167 | $typeSchema->type = $schema->disallow; 168 | $this->checkType($value, $typeSchema, $path); 169 | 170 | // if no new errors were raised it must be a disallowed value 171 | if (count($this->getErrors()) == count($initErrors)) { 172 | $this->addError(ConstraintError::DISALLOW(), $path); 173 | } else { 174 | $this->errors = $initErrors; 175 | } 176 | } 177 | 178 | if (isset($schema->not)) { 179 | $initErrors = $this->getErrors(); 180 | $this->checkUndefined($value, $schema->not, $path, $i); 181 | 182 | // if no new errors were raised then the instance validated against the "not" schema 183 | if (count($this->getErrors()) == count($initErrors)) { 184 | $this->addError(ConstraintError::NOT(), $path); 185 | } else { 186 | $this->errors = $initErrors; 187 | } 188 | } 189 | 190 | // Verify that dependencies are met 191 | if (isset($schema->dependencies) && $this->getTypeCheck()->isObject($value)) { 192 | $this->validateDependencies($value, $schema->dependencies, $path); 193 | } 194 | } 195 | 196 | /** 197 | * Check whether a default should be applied for this value 198 | * 199 | * @param mixed $schema 200 | * @param mixed $parentSchema 201 | * @param bool $requiredOnly 202 | * 203 | * @return bool 204 | */ 205 | private function shouldApplyDefaultValue($requiredOnly, $schema, $name = null, $parentSchema = null) 206 | { 207 | // required-only mode is off 208 | if (!$requiredOnly) { 209 | return true; 210 | } 211 | // draft-04 required is set 212 | if ( 213 | $name !== null 214 | && isset($parentSchema->required) 215 | && is_array($parentSchema->required) 216 | && in_array($name, $parentSchema->required) 217 | ) { 218 | return true; 219 | } 220 | // draft-03 required is set 221 | if (isset($schema->required) && !is_array($schema->required) && $schema->required) { 222 | return true; 223 | } 224 | // default case 225 | return false; 226 | } 227 | 228 | /** 229 | * Apply default values 230 | * 231 | * @param mixed $value 232 | * @param mixed $schema 233 | * @param JsonPointer $path 234 | */ 235 | protected function applyDefaultValues(&$value, $schema, $path): void 236 | { 237 | // only apply defaults if feature is enabled 238 | if (!$this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) { 239 | return; 240 | } 241 | 242 | // apply defaults if appropriate 243 | $requiredOnly = (bool) $this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_DEFAULTS); 244 | if (isset($schema->properties) && LooseTypeCheck::isObject($value)) { 245 | // $value is an object or assoc array, and properties are defined - treat as an object 246 | foreach ($schema->properties as $currentProperty => $propertyDefinition) { 247 | $propertyDefinition = $this->factory->getSchemaStorage()->resolveRefSchema($propertyDefinition); 248 | if ( 249 | !LooseTypeCheck::propertyExists($value, $currentProperty) 250 | && property_exists($propertyDefinition, 'default') 251 | && $this->shouldApplyDefaultValue($requiredOnly, $propertyDefinition, $currentProperty, $schema) 252 | ) { 253 | // assign default value 254 | if (is_object($propertyDefinition->default)) { 255 | LooseTypeCheck::propertySet($value, $currentProperty, clone $propertyDefinition->default); 256 | } else { 257 | LooseTypeCheck::propertySet($value, $currentProperty, $propertyDefinition->default); 258 | } 259 | $this->appliedDefaults[] = $currentProperty; 260 | } 261 | } 262 | } elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) { 263 | $items = []; 264 | if (LooseTypeCheck::isArray($schema->items)) { 265 | $items = $schema->items; 266 | } elseif (isset($schema->minItems) && count($value) < $schema->minItems) { 267 | $items = array_fill(count($value), $schema->minItems - count($value), $schema->items); 268 | } 269 | // $value is an array, and items are defined - treat as plain array 270 | foreach ($items as $currentItem => $itemDefinition) { 271 | $itemDefinition = $this->factory->getSchemaStorage()->resolveRefSchema($itemDefinition); 272 | if ( 273 | !array_key_exists($currentItem, $value) 274 | && property_exists($itemDefinition, 'default') 275 | && $this->shouldApplyDefaultValue($requiredOnly, $itemDefinition)) { 276 | if (is_object($itemDefinition->default)) { 277 | $value[$currentItem] = clone $itemDefinition->default; 278 | } else { 279 | $value[$currentItem] = $itemDefinition->default; 280 | } 281 | } 282 | $path->setFromDefault(); 283 | } 284 | } elseif ( 285 | $value instanceof self 286 | && property_exists($schema, 'default') 287 | && $this->shouldApplyDefaultValue($requiredOnly, $schema)) { 288 | // $value is a leaf, not a container - apply the default directly 289 | $value = is_object($schema->default) ? clone $schema->default : $schema->default; 290 | $path->setFromDefault(); 291 | } 292 | } 293 | 294 | /** 295 | * Validate allOf, anyOf, and oneOf properties 296 | * 297 | * @param mixed $value 298 | * @param mixed $schema 299 | * @param JsonPointer $path 300 | * @param string $i 301 | */ 302 | protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i = '') 303 | { 304 | // Verify type 305 | if ($value instanceof self) { 306 | return; 307 | } 308 | 309 | if (isset($schema->allOf)) { 310 | $isValid = true; 311 | foreach ($schema->allOf as $allOf) { 312 | $initErrors = $this->getErrors(); 313 | $this->checkUndefined($value, $allOf, $path, $i); 314 | $isValid = $isValid && (count($this->getErrors()) == count($initErrors)); 315 | } 316 | if (!$isValid) { 317 | $this->addError(ConstraintError::ALL_OF(), $path); 318 | } 319 | } 320 | 321 | if (isset($schema->anyOf)) { 322 | $isValid = false; 323 | $startErrors = $this->getErrors(); 324 | $coerceOrDefaults = $this->factory->getConfig(self::CHECK_MODE_COERCE_TYPES | self::CHECK_MODE_APPLY_DEFAULTS); 325 | 326 | foreach ($schema->anyOf as $anyOf) { 327 | $initErrors = $this->getErrors(); 328 | try { 329 | $anyOfValue = $coerceOrDefaults ? DeepCopy::copyOf($value) : $value; 330 | $this->checkUndefined($anyOfValue, $anyOf, $path, $i); 331 | if ($isValid = (count($this->getErrors()) === count($initErrors))) { 332 | $value = $anyOfValue; 333 | break; 334 | } 335 | } catch (ValidationException $e) { 336 | $isValid = false; 337 | } 338 | } 339 | if (!$isValid) { 340 | $this->addError(ConstraintError::ANY_OF(), $path); 341 | } else { 342 | $this->errors = $startErrors; 343 | } 344 | } 345 | 346 | if (isset($schema->oneOf)) { 347 | $allErrors = []; 348 | $matchedSchemas = []; 349 | $startErrors = $this->getErrors(); 350 | $coerceOrDefaults = $this->factory->getConfig(self::CHECK_MODE_COERCE_TYPES | self::CHECK_MODE_APPLY_DEFAULTS); 351 | 352 | foreach ($schema->oneOf as $oneOf) { 353 | try { 354 | $this->errors = []; 355 | 356 | $oneOfValue = $coerceOrDefaults ? DeepCopy::copyOf($value) : $value; 357 | $this->checkUndefined($oneOfValue, $oneOf, $path, $i); 358 | if (count($this->getErrors()) === 0) { 359 | $matchedSchemas[] = ['schema' => $oneOf, 'value' => $oneOfValue]; 360 | } 361 | $allErrors = array_merge($allErrors, array_values($this->getErrors())); 362 | } catch (ValidationException $e) { 363 | // deliberately do nothing here - validation failed, but we want to check 364 | // other schema options in the OneOf field. 365 | } 366 | } 367 | if (count($matchedSchemas) !== 1) { 368 | $this->addErrors(array_merge($allErrors, $startErrors)); 369 | $this->addError(ConstraintError::ONE_OF(), $path); 370 | } else { 371 | $this->errors = $startErrors; 372 | $value = $matchedSchemas[0]['value']; 373 | } 374 | } 375 | } 376 | 377 | /** 378 | * Validate dependencies 379 | * 380 | * @param mixed $value 381 | * @param mixed $dependencies 382 | * @param JsonPointer $path 383 | * @param string $i 384 | */ 385 | protected function validateDependencies($value, $dependencies, JsonPointer $path, $i = '') 386 | { 387 | foreach ($dependencies as $key => $dependency) { 388 | if ($this->getTypeCheck()->propertyExists($value, $key)) { 389 | if (is_string($dependency)) { 390 | // Draft 3 string is allowed - e.g. "dependencies": {"bar": "foo"} 391 | if (!$this->getTypeCheck()->propertyExists($value, $dependency)) { 392 | $this->addError(ConstraintError::DEPENDENCIES(), $path, [ 393 | 'key' => $key, 394 | 'dependency' => $dependency 395 | ]); 396 | } 397 | } elseif (is_array($dependency)) { 398 | // Draft 4 must be an array - e.g. "dependencies": {"bar": ["foo"]} 399 | foreach ($dependency as $d) { 400 | if (!$this->getTypeCheck()->propertyExists($value, $d)) { 401 | $this->addError(ConstraintError::DEPENDENCIES(), $path, [ 402 | 'key' => $key, 403 | 'dependency' => $dependency 404 | ]); 405 | } 406 | } 407 | } elseif (is_object($dependency)) { 408 | // Schema - e.g. "dependencies": {"bar": {"properties": {"foo": {...}}}} 409 | $this->checkUndefined($value, $dependency, $path, $i); 410 | } 411 | } 412 | } 413 | } 414 | 415 | protected function validateUri($schema, $schemaUri = null) 416 | { 417 | $resolver = new UriResolver(); 418 | $retriever = $this->factory->getUriRetriever(); 419 | 420 | $jsonSchema = null; 421 | if ($resolver->isValid($schemaUri)) { 422 | $schemaId = property_exists($schema, 'id') ? $schema->id : null; 423 | $jsonSchema = $retriever->retrieve($schemaId, $schemaUri); 424 | } 425 | 426 | return $jsonSchema; 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /src/JsonSchema/Entity/JsonPointer.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class JsonPointer 22 | { 23 | /** @var string */ 24 | private $filename; 25 | 26 | /** @var string[] */ 27 | private $propertyPaths = []; 28 | 29 | /** 30 | * @var bool Whether the value at this path was set from a schema default 31 | */ 32 | private $fromDefault = false; 33 | 34 | /** 35 | * @param string $value 36 | * 37 | * @throws InvalidArgumentException when $value is not a string 38 | */ 39 | public function __construct($value) 40 | { 41 | if (!is_string($value)) { 42 | throw new InvalidArgumentException('Ref value must be a string'); 43 | } 44 | 45 | $splitRef = explode('#', $value, 2); 46 | $this->filename = $splitRef[0]; 47 | if (array_key_exists(1, $splitRef)) { 48 | $this->propertyPaths = $this->decodePropertyPaths($splitRef[1]); 49 | } 50 | } 51 | 52 | /** 53 | * @param string $propertyPathString 54 | * 55 | * @return string[] 56 | */ 57 | private function decodePropertyPaths($propertyPathString) 58 | { 59 | $paths = []; 60 | foreach (explode('/', trim($propertyPathString, '/')) as $path) { 61 | $path = $this->decodePath($path); 62 | if (is_string($path) && '' !== $path) { 63 | $paths[] = $path; 64 | } 65 | } 66 | 67 | return $paths; 68 | } 69 | 70 | /** 71 | * @return array 72 | */ 73 | private function encodePropertyPaths() 74 | { 75 | return array_map( 76 | [$this, 'encodePath'], 77 | $this->getPropertyPaths() 78 | ); 79 | } 80 | 81 | /** 82 | * @param string $path 83 | * 84 | * @return string 85 | */ 86 | private function decodePath($path) 87 | { 88 | return strtr($path, ['~1' => '/', '~0' => '~', '%25' => '%']); 89 | } 90 | 91 | /** 92 | * @param string $path 93 | * 94 | * @return string 95 | */ 96 | private function encodePath($path) 97 | { 98 | return strtr($path, ['/' => '~1', '~' => '~0', '%' => '%25']); 99 | } 100 | 101 | /** 102 | * @return string 103 | */ 104 | public function getFilename() 105 | { 106 | return $this->filename; 107 | } 108 | 109 | /** 110 | * @return string[] 111 | */ 112 | public function getPropertyPaths() 113 | { 114 | return $this->propertyPaths; 115 | } 116 | 117 | /** 118 | * @param array $propertyPaths 119 | * 120 | * @return JsonPointer 121 | */ 122 | public function withPropertyPaths(array $propertyPaths) 123 | { 124 | $new = clone $this; 125 | $new->propertyPaths = array_map(function ($p): string { return (string) $p; }, $propertyPaths); 126 | 127 | return $new; 128 | } 129 | 130 | /** 131 | * @return string 132 | */ 133 | public function getPropertyPathAsString() 134 | { 135 | return rtrim('#/' . implode('/', $this->encodePropertyPaths()), '/'); 136 | } 137 | 138 | /** 139 | * @return string 140 | */ 141 | public function __toString() 142 | { 143 | return $this->getFilename() . $this->getPropertyPathAsString(); 144 | } 145 | 146 | /** 147 | * Mark the value at this path as being set from a schema default 148 | */ 149 | public function setFromDefault(): void 150 | { 151 | $this->fromDefault = true; 152 | } 153 | 154 | /** 155 | * Check whether the value at this path was set from a schema default 156 | * 157 | * @return bool 158 | */ 159 | public function fromDefault() 160 | { 161 | return $this->fromDefault; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/JsonSchema/Enum.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class UnresolvableJsonPointerException extends InvalidArgumentException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /src/JsonSchema/Exception/UriResolverException.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class ObjectIterator implements \Iterator, \Countable 20 | { 21 | /** @var object */ 22 | private $object; 23 | 24 | /** @var int */ 25 | private $position = 0; 26 | 27 | /** @var array */ 28 | private $data = []; 29 | 30 | /** @var bool */ 31 | private $initialized = false; 32 | 33 | /** 34 | * @param object $object 35 | */ 36 | public function __construct($object) 37 | { 38 | $this->object = $object; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | #[\ReturnTypeWillChange] 45 | public function current() 46 | { 47 | $this->initialize(); 48 | 49 | return $this->data[$this->position]; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function next(): void 56 | { 57 | $this->initialize(); 58 | $this->position++; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function key(): int 65 | { 66 | $this->initialize(); 67 | 68 | return $this->position; 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public function valid(): bool 75 | { 76 | $this->initialize(); 77 | 78 | return isset($this->data[$this->position]); 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function rewind(): void 85 | { 86 | $this->initialize(); 87 | $this->position = 0; 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | public function count(): int 94 | { 95 | $this->initialize(); 96 | 97 | return count($this->data); 98 | } 99 | 100 | /** 101 | * Initializer 102 | */ 103 | private function initialize() 104 | { 105 | if (!$this->initialized) { 106 | $this->data = $this->buildDataFromObject($this->object); 107 | $this->initialized = true; 108 | } 109 | } 110 | 111 | /** 112 | * @param object $object 113 | * 114 | * @return array 115 | */ 116 | private function buildDataFromObject($object) 117 | { 118 | $result = []; 119 | 120 | $stack = new \SplStack(); 121 | $stack->push($object); 122 | 123 | while (!$stack->isEmpty()) { 124 | $current = $stack->pop(); 125 | if (is_object($current)) { 126 | array_push($result, $current); 127 | } 128 | 129 | foreach ($this->getDataFromItem($current) as $propertyName => $propertyValue) { 130 | if (is_object($propertyValue) || is_array($propertyValue)) { 131 | $stack->push($propertyValue); 132 | } 133 | } 134 | } 135 | 136 | return $result; 137 | } 138 | 139 | /** 140 | * @param object|array $item 141 | * 142 | * @return array 143 | */ 144 | private function getDataFromItem($item) 145 | { 146 | if (!is_object($item) && !is_array($item)) { 147 | return []; 148 | } 149 | 150 | return is_object($item) ? get_object_vars($item) : $item; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/JsonSchema/Rfc3339.php: -------------------------------------------------------------------------------- 1 | uriRetriever = $uriRetriever ?: new UriRetriever(); 26 | $this->uriResolver = $uriResolver ?: new UriResolver(); 27 | } 28 | 29 | /** 30 | * @return UriRetrieverInterface 31 | */ 32 | public function getUriRetriever() 33 | { 34 | return $this->uriRetriever; 35 | } 36 | 37 | /** 38 | * @return UriResolverInterface 39 | */ 40 | public function getUriResolver() 41 | { 42 | return $this->uriResolver; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function addSchema(string $id, $schema = null): void 49 | { 50 | if (is_null($schema) && $id !== self::INTERNAL_PROVIDED_SCHEMA_URI) { 51 | // if the schema was user-provided to Validator and is still null, then assume this is 52 | // what the user intended, as there's no way for us to retrieve anything else. User-supplied 53 | // schemas do not have an associated URI when passed via Validator::validate(). 54 | $schema = $this->uriRetriever->retrieve($id); 55 | } 56 | 57 | // cast array schemas to object 58 | if (is_array($schema)) { 59 | $schema = BaseConstraint::arrayToObjectRecursive($schema); 60 | } 61 | 62 | // workaround for bug in draft-03 & draft-04 meta-schemas (id & $ref defined with incorrect format) 63 | // see https://github.com/json-schema-org/JSON-Schema-Test-Suite/issues/177#issuecomment-293051367 64 | if (is_object($schema) && property_exists($schema, 'id')) { 65 | if ($schema->id === 'http://json-schema.org/draft-04/schema#') { 66 | $schema->properties->id->format = 'uri-reference'; 67 | } elseif ($schema->id === 'http://json-schema.org/draft-03/schema#') { 68 | $schema->properties->id->format = 'uri-reference'; 69 | $schema->properties->{'$ref'}->format = 'uri-reference'; 70 | } 71 | } 72 | 73 | $this->scanForSubschemas($schema, $id); 74 | 75 | // resolve references 76 | $this->expandRefs($schema, $id); 77 | 78 | $this->schemas[$id] = $schema; 79 | } 80 | 81 | /** 82 | * Recursively resolve all references against the provided base 83 | * 84 | * @param mixed $schema 85 | */ 86 | private function expandRefs(&$schema, ?string $parentId = null): void 87 | { 88 | if (!is_object($schema)) { 89 | if (is_array($schema)) { 90 | foreach ($schema as &$member) { 91 | $this->expandRefs($member, $parentId); 92 | } 93 | } 94 | 95 | return; 96 | } 97 | 98 | if (property_exists($schema, '$ref') && is_string($schema->{'$ref'})) { 99 | $refPointer = new JsonPointer($this->uriResolver->resolve($schema->{'$ref'}, $parentId)); 100 | $schema->{'$ref'} = (string) $refPointer; 101 | } 102 | 103 | foreach ($schema as $propertyName => &$member) { 104 | if (in_array($propertyName, ['enum', 'const'])) { 105 | // Enum and const don't allow $ref as a keyword, see https://github.com/json-schema-org/JSON-Schema-Test-Suite/pull/445 106 | continue; 107 | } 108 | 109 | $childId = $parentId; 110 | if (property_exists($schema, 'id') && is_string($schema->id) && $childId !== $schema->id) { 111 | $childId = $this->uriResolver->resolve($schema->id, $childId); 112 | } 113 | 114 | $this->expandRefs($member, $childId); 115 | } 116 | } 117 | 118 | /** 119 | * {@inheritdoc} 120 | */ 121 | public function getSchema(string $id) 122 | { 123 | if (!array_key_exists($id, $this->schemas)) { 124 | $this->addSchema($id); 125 | } 126 | 127 | return $this->schemas[$id]; 128 | } 129 | 130 | /** 131 | * {@inheritdoc} 132 | */ 133 | public function resolveRef(string $ref, $resolveStack = []) 134 | { 135 | $jsonPointer = new JsonPointer($ref); 136 | 137 | // resolve filename for pointer 138 | $fileName = $jsonPointer->getFilename(); 139 | if (!strlen($fileName)) { 140 | throw new UnresolvableJsonPointerException(sprintf( 141 | "Could not resolve fragment '%s': no file is defined", 142 | $jsonPointer->getPropertyPathAsString() 143 | )); 144 | } 145 | 146 | // get & process the schema 147 | $refSchema = $this->getSchema($fileName); 148 | foreach ($jsonPointer->getPropertyPaths() as $path) { 149 | if (is_object($refSchema) && property_exists($refSchema, $path)) { 150 | $refSchema = $this->resolveRefSchema($refSchema->{$path}, $resolveStack); 151 | } elseif (is_array($refSchema) && array_key_exists($path, $refSchema)) { 152 | $refSchema = $this->resolveRefSchema($refSchema[$path], $resolveStack); 153 | } else { 154 | throw new UnresolvableJsonPointerException(sprintf( 155 | 'File: %s is found, but could not resolve fragment: %s', 156 | $jsonPointer->getFilename(), 157 | $jsonPointer->getPropertyPathAsString() 158 | )); 159 | } 160 | } 161 | 162 | return $refSchema; 163 | } 164 | 165 | /** 166 | * {@inheritdoc} 167 | */ 168 | public function resolveRefSchema($refSchema, $resolveStack = []) 169 | { 170 | if (is_object($refSchema) && property_exists($refSchema, '$ref') && is_string($refSchema->{'$ref'})) { 171 | if (in_array($refSchema, $resolveStack, true)) { 172 | throw new UnresolvableJsonPointerException(sprintf( 173 | 'Dereferencing a pointer to %s results in an infinite loop', 174 | $refSchema->{'$ref'} 175 | )); 176 | } 177 | $resolveStack[] = $refSchema; 178 | 179 | return $this->resolveRef($refSchema->{'$ref'}, $resolveStack); 180 | } 181 | 182 | return $refSchema; 183 | } 184 | 185 | /** 186 | * @param mixed $schema 187 | */ 188 | private function scanForSubschemas($schema, string $parentId): void 189 | { 190 | if (!$schema instanceof \stdClass && !is_array($schema)) { 191 | return; 192 | } 193 | 194 | foreach ($schema as $propertyName => $potentialSubSchema) { 195 | if (!is_object($potentialSubSchema)) { 196 | continue; 197 | } 198 | 199 | if (property_exists($potentialSubSchema, 'id') && is_string($potentialSubSchema->id) && property_exists($potentialSubSchema, 'type')) { 200 | // Enum and const don't allow id as a keyword, see https://github.com/json-schema-org/JSON-Schema-Test-Suite/pull/471 201 | if (in_array($propertyName, ['enum', 'const'])) { 202 | continue; 203 | } 204 | 205 | // Found sub schema 206 | $this->addSchema($this->uriResolver->resolve($potentialSubSchema->id, $parentId), $potentialSubSchema); 207 | } 208 | 209 | $this->scanForSubschemas($potentialSubSchema, $parentId); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/JsonSchema/SchemaStorageInterface.php: -------------------------------------------------------------------------------- 1 | $left 52 | * @param array $right 53 | */ 54 | private static function isArrayEqual(array $left, array $right): bool 55 | { 56 | if (count($left) !== count($right)) { 57 | return false; 58 | } 59 | foreach ($left as $key => $value) { 60 | if (!array_key_exists($key, $right)) { 61 | return false; 62 | } 63 | 64 | if (!self::isEqual($value, $right[$key])) { 65 | return false; 66 | } 67 | } 68 | 69 | return true; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/JsonSchema/Tool/DeepCopy.php: -------------------------------------------------------------------------------- 1 | 65535)) { 40 | return false; 41 | } 42 | 43 | // Validate path (reject illegal characters: < > { } | \ ^ `) 44 | if (!empty($matches[6]) && preg_match('/[<>{}|\\\^`]/', $matches[6])) { 45 | return false; 46 | } 47 | 48 | return true; 49 | } 50 | 51 | // If not hierarchical, check non-hierarchical URIs 52 | if (preg_match($nonHierarchicalPattern, $uri, $matches) === 1) { 53 | $scheme = strtolower($matches[1]); // Extract the scheme 54 | 55 | // Special case: `mailto:` must contain a **valid email address** 56 | if ($scheme === 'mailto') { 57 | return preg_match($emailPattern, $matches[2]) === 1; 58 | } 59 | 60 | return true; // Valid non-hierarchical URI 61 | } 62 | 63 | return false; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/JsonSchema/Uri/Retrievers/AbstractRetriever.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | abstract class AbstractRetriever implements UriRetrieverInterface 20 | { 21 | /** 22 | * Media content type 23 | * 24 | * @var string 25 | */ 26 | protected $contentType; 27 | 28 | /** 29 | * {@inheritdoc} 30 | * 31 | * @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::getContentType() 32 | */ 33 | public function getContentType() 34 | { 35 | return $this->contentType; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/JsonSchema/Uri/Retrievers/Curl.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class Curl extends AbstractRetriever 23 | { 24 | protected $messageBody; 25 | 26 | public function __construct() 27 | { 28 | if (!function_exists('curl_init')) { 29 | // Cannot test this, because curl_init is present on all test platforms plus mock 30 | throw new RuntimeException('cURL not installed'); // @codeCoverageIgnore 31 | } 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | * 37 | * @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::retrieve() 38 | */ 39 | public function retrieve($uri) 40 | { 41 | $ch = curl_init(); 42 | 43 | curl_setopt($ch, CURLOPT_URL, $uri); 44 | curl_setopt($ch, CURLOPT_HEADER, true); 45 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 46 | curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: ' . Validator::SCHEMA_MEDIA_TYPE]); 47 | 48 | $response = curl_exec($ch); 49 | if (false === $response) { 50 | throw new \JsonSchema\Exception\ResourceNotFoundException('JSON schema not found'); 51 | } 52 | 53 | $this->fetchMessageBody($response); 54 | $this->fetchContentType($response); 55 | 56 | curl_close($ch); 57 | 58 | return $this->messageBody; 59 | } 60 | 61 | /** 62 | * @param string $response cURL HTTP response 63 | */ 64 | private function fetchMessageBody($response) 65 | { 66 | preg_match("/(?:\r\n){2}(.*)$/ms", $response, $match); 67 | $this->messageBody = $match[1]; 68 | } 69 | 70 | /** 71 | * @param string $response cURL HTTP response 72 | * 73 | * @return bool Whether the Content-Type header was found or not 74 | */ 75 | protected function fetchContentType($response) 76 | { 77 | if (0 < preg_match("/Content-Type:(\V*)/ims", $response, $match)) { 78 | $this->contentType = trim($match[1]); 79 | 80 | return true; 81 | } 82 | 83 | return false; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/JsonSchema/Uri/Retrievers/FileGetContents.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class FileGetContents extends AbstractRetriever 22 | { 23 | protected $messageBody; 24 | 25 | /** 26 | * {@inheritdoc} 27 | * 28 | * @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::retrieve() 29 | */ 30 | public function retrieve($uri) 31 | { 32 | $errorMessage = null; 33 | set_error_handler(function ($errno, $errstr) use (&$errorMessage) { 34 | $errorMessage = $errstr; 35 | }); 36 | $response = file_get_contents($uri); 37 | restore_error_handler(); 38 | 39 | if ($errorMessage) { 40 | throw new ResourceNotFoundException($errorMessage); 41 | } 42 | 43 | if (false === $response) { 44 | throw new ResourceNotFoundException('JSON schema not found at ' . $uri); 45 | } 46 | 47 | if ($response == '' 48 | && substr($uri, 0, 7) == 'file://' && substr($uri, -1) == '/' 49 | ) { 50 | throw new ResourceNotFoundException('JSON schema not found at ' . $uri); 51 | } 52 | 53 | $this->messageBody = $response; 54 | if (!empty($http_response_header)) { 55 | // $http_response_header cannot be tested, because it's defined in the method's local scope 56 | // See http://php.net/manual/en/reserved.variables.httpresponseheader.php for more info. 57 | $this->fetchContentType($http_response_header); // @codeCoverageIgnore 58 | } else { // @codeCoverageIgnore 59 | // Could be a "file://" url or something else - fake up the response 60 | $this->contentType = null; 61 | } 62 | 63 | return $this->messageBody; 64 | } 65 | 66 | /** 67 | * @param array $headers HTTP Response Headers 68 | * 69 | * @return bool Whether the Content-Type header was found or not 70 | */ 71 | private function fetchContentType(array $headers) 72 | { 73 | foreach (array_reverse($headers) as $header) { 74 | if ($this->contentType = self::getContentTypeMatchInHeader($header)) { 75 | return true; 76 | } 77 | } 78 | 79 | return false; 80 | } 81 | 82 | /** 83 | * @param string $header 84 | * 85 | * @return string|null 86 | */ 87 | protected static function getContentTypeMatchInHeader($header) 88 | { 89 | if (0 < preg_match("/Content-Type:(\V*)/ims", $header, $match)) { 90 | return trim($match[1]); 91 | } 92 | 93 | return null; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/JsonSchema/Uri/Retrievers/PredefinedArray.php: -------------------------------------------------------------------------------- 1 | '{ ... }', 16 | * 'http://acme.com/schemas/address#' => '{ ... }', 17 | * )) 18 | * 19 | * $schema = $retriever->retrieve('http://acme.com/schemas/person#'); 20 | */ 21 | class PredefinedArray extends AbstractRetriever 22 | { 23 | /** 24 | * Contains schemas as URI => JSON 25 | * 26 | * @var array 27 | */ 28 | private $schemas; 29 | 30 | /** 31 | * Constructor 32 | * 33 | * @param array $schemas 34 | * @param string $contentType 35 | */ 36 | public function __construct(array $schemas, $contentType = Validator::SCHEMA_MEDIA_TYPE) 37 | { 38 | $this->schemas = $schemas; 39 | $this->contentType = $contentType; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | * 45 | * @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::retrieve() 46 | */ 47 | public function retrieve($uri) 48 | { 49 | if (!array_key_exists($uri, $this->schemas)) { 50 | throw new \JsonSchema\Exception\ResourceNotFoundException(sprintf( 51 | 'The JSON schema "%s" was not found.', 52 | $uri 53 | )); 54 | } 55 | 56 | return $this->schemas[$uri]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/JsonSchema/Uri/Retrievers/UriRetrieverInterface.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | interface UriRetrieverInterface 20 | { 21 | /** 22 | * Retrieve a schema from the specified URI 23 | * 24 | * @param string $uri URI that resolves to a JSON schema 25 | * 26 | * @throws \JsonSchema\Exception\ResourceNotFoundException 27 | * 28 | * @return mixed string|null 29 | */ 30 | public function retrieve($uri); 31 | 32 | /** 33 | * Get media content type 34 | * 35 | * @return string 36 | */ 37 | public function getContentType(); 38 | } 39 | -------------------------------------------------------------------------------- /src/JsonSchema/Uri/UriResolver.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class UriResolver implements UriResolverInterface 23 | { 24 | /** 25 | * Parses a URI into five main components 26 | * 27 | * @param string $uri 28 | * 29 | * @return array 30 | */ 31 | public function parse($uri) 32 | { 33 | preg_match('|^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?|', (string) $uri, $match); 34 | 35 | $components = []; 36 | if (5 < count($match)) { 37 | $components = [ 38 | 'scheme' => $match[2], 39 | 'authority' => $match[4], 40 | 'path' => $match[5] 41 | ]; 42 | } 43 | if (7 < count($match)) { 44 | $components['query'] = $match[7]; 45 | } 46 | if (9 < count($match)) { 47 | $components['fragment'] = $match[9]; 48 | } 49 | 50 | return $components; 51 | } 52 | 53 | /** 54 | * Builds a URI based on n array with the main components 55 | * 56 | * @param array $components 57 | * 58 | * @return string 59 | */ 60 | public function generate(array $components) 61 | { 62 | $uri = $components['scheme'] . '://' 63 | . $components['authority'] 64 | . $components['path']; 65 | 66 | if (array_key_exists('query', $components) && strlen($components['query'])) { 67 | $uri .= '?' . $components['query']; 68 | } 69 | if (array_key_exists('fragment', $components)) { 70 | $uri .= '#' . $components['fragment']; 71 | } 72 | 73 | return $uri; 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function resolve($uri, $baseUri = null) 80 | { 81 | // treat non-uri base as local file path 82 | if ( 83 | !is_null($baseUri) && 84 | !filter_var($baseUri, \FILTER_VALIDATE_URL) && 85 | !preg_match('|^[^/]+://|u', $baseUri) 86 | ) { 87 | if (is_file($baseUri)) { 88 | $baseUri = 'file://' . realpath($baseUri); 89 | } elseif (is_dir($baseUri)) { 90 | $baseUri = 'file://' . realpath($baseUri) . '/'; 91 | } else { 92 | $baseUri = 'file://' . getcwd() . '/' . $baseUri; 93 | } 94 | } 95 | 96 | if ($uri == '') { 97 | return $baseUri; 98 | } 99 | 100 | $components = $this->parse($uri); 101 | $path = $components['path']; 102 | 103 | if (!empty($components['scheme'])) { 104 | return $uri; 105 | } 106 | $baseComponents = $this->parse($baseUri); 107 | $basePath = $baseComponents['path']; 108 | 109 | $baseComponents['path'] = self::combineRelativePathWithBasePath($path, $basePath); 110 | if (isset($components['fragment'])) { 111 | $baseComponents['fragment'] = $components['fragment']; 112 | } 113 | 114 | return $this->generate($baseComponents); 115 | } 116 | 117 | /** 118 | * Tries to glue a relative path onto an absolute one 119 | * 120 | * @param string $relativePath 121 | * @param string $basePath 122 | * 123 | * @throws UriResolverException 124 | * 125 | * @return string Merged path 126 | */ 127 | public static function combineRelativePathWithBasePath($relativePath, $basePath) 128 | { 129 | $relativePath = self::normalizePath($relativePath); 130 | if (!$relativePath) { 131 | return $basePath; 132 | } 133 | if ($relativePath[0] === '/') { 134 | return $relativePath; 135 | } 136 | if (!$basePath) { 137 | throw new UriResolverException(sprintf("Unable to resolve URI '%s' from base '%s'", $relativePath, $basePath)); 138 | } 139 | 140 | $dirname = $basePath[strlen($basePath) - 1] === '/' ? $basePath : dirname($basePath); 141 | $combined = rtrim($dirname, '/') . '/' . ltrim($relativePath, '/'); 142 | $combinedSegments = explode('/', $combined); 143 | $collapsedSegments = []; 144 | while ($combinedSegments) { 145 | $segment = array_shift($combinedSegments); 146 | if ($segment === '..') { 147 | if (count($collapsedSegments) <= 1) { 148 | // Do not remove the top level (domain) 149 | // This is not ideal - the domain should not be part of the path here. parse() and generate() 150 | // should handle the "domain" separately, like the schema. 151 | // Then the if-condition here would be `if (!$collapsedSegments) {`. 152 | throw new UriResolverException(sprintf("Unable to resolve URI '%s' from base '%s'", $relativePath, $basePath)); 153 | } 154 | array_pop($collapsedSegments); 155 | } else { 156 | $collapsedSegments[] = $segment; 157 | } 158 | } 159 | 160 | return implode('/', $collapsedSegments); 161 | } 162 | 163 | /** 164 | * Normalizes a URI path component by removing dot-slash and double slashes 165 | * 166 | * @param string $path 167 | * 168 | * @return string 169 | */ 170 | private static function normalizePath($path) 171 | { 172 | $path = preg_replace('|((?parse($uri); 186 | 187 | return !empty($components); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/JsonSchema/Uri/UriRetriever.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | class UriRetriever implements BaseUriRetrieverInterface 28 | { 29 | /** 30 | * @var array Map of URL translations 31 | */ 32 | protected $translationMap = [ 33 | // use local copies of the spec schemas 34 | '|^https?://json-schema.org/draft-(0[34])/schema#?|' => 'package://dist/schema/json-schema-draft-$1.json' 35 | ]; 36 | 37 | /** 38 | * @var array A list of endpoints for media type check exclusion 39 | */ 40 | protected $allowedInvalidContentTypeEndpoints = [ 41 | 'http://json-schema.org/', 42 | 'https://json-schema.org/' 43 | ]; 44 | 45 | /** 46 | * @var null|UriRetrieverInterface 47 | */ 48 | protected $uriRetriever = null; 49 | 50 | /** 51 | * @var array|object[] 52 | * 53 | * @see loadSchema 54 | */ 55 | private $schemaCache = []; 56 | 57 | /** 58 | * Adds an endpoint to the media type validation exclusion list 59 | * 60 | * @param string $endpoint 61 | */ 62 | public function addInvalidContentTypeEndpoint($endpoint) 63 | { 64 | $this->allowedInvalidContentTypeEndpoints[] = $endpoint; 65 | } 66 | 67 | /** 68 | * Guarantee the correct media type was encountered 69 | * 70 | * @param UriRetrieverInterface $uriRetriever 71 | * @param string $uri 72 | * 73 | * @return bool|void 74 | */ 75 | public function confirmMediaType($uriRetriever, $uri) 76 | { 77 | $contentType = $uriRetriever->getContentType(); 78 | 79 | if (is_null($contentType)) { 80 | // Well, we didn't get an invalid one 81 | return; 82 | } 83 | 84 | if (in_array($contentType, [Validator::SCHEMA_MEDIA_TYPE, 'application/json'])) { 85 | return; 86 | } 87 | 88 | foreach ($this->allowedInvalidContentTypeEndpoints as $endpoint) { 89 | if (!\is_null($uri) && strpos($uri, $endpoint) === 0) { 90 | return true; 91 | } 92 | } 93 | 94 | throw new InvalidSchemaMediaTypeException(sprintf('Media type %s expected', Validator::SCHEMA_MEDIA_TYPE)); 95 | } 96 | 97 | /** 98 | * Get a URI Retriever 99 | * 100 | * If none is specified, sets a default FileGetContents retriever and 101 | * returns that object. 102 | * 103 | * @return UriRetrieverInterface 104 | */ 105 | public function getUriRetriever() 106 | { 107 | if (is_null($this->uriRetriever)) { 108 | $this->setUriRetriever(new FileGetContents()); 109 | } 110 | 111 | return $this->uriRetriever; 112 | } 113 | 114 | /** 115 | * Resolve a schema based on pointer 116 | * 117 | * URIs can have a fragment at the end in the format of 118 | * #/path/to/object and we are to look up the 'path' property of 119 | * the first object then the 'to' and 'object' properties. 120 | * 121 | * @param object $jsonSchema JSON Schema contents 122 | * @param string $uri JSON Schema URI 123 | * 124 | * @throws ResourceNotFoundException 125 | * 126 | * @return object JSON Schema after walking down the fragment pieces 127 | */ 128 | public function resolvePointer($jsonSchema, $uri) 129 | { 130 | $resolver = new UriResolver(); 131 | $parsed = $resolver->parse($uri); 132 | if (empty($parsed['fragment'])) { 133 | return $jsonSchema; 134 | } 135 | 136 | $path = explode('/', $parsed['fragment']); 137 | while ($path) { 138 | $pathElement = array_shift($path); 139 | if (!empty($pathElement)) { 140 | $pathElement = str_replace('~1', '/', $pathElement); 141 | $pathElement = str_replace('~0', '~', $pathElement); 142 | if (!empty($jsonSchema->$pathElement)) { 143 | $jsonSchema = $jsonSchema->$pathElement; 144 | } else { 145 | throw new ResourceNotFoundException( 146 | 'Fragment "' . $parsed['fragment'] . '" not found' 147 | . ' in ' . $uri 148 | ); 149 | } 150 | 151 | if (!is_object($jsonSchema)) { 152 | throw new ResourceNotFoundException( 153 | 'Fragment part "' . $pathElement . '" is no object ' 154 | . ' in ' . $uri 155 | ); 156 | } 157 | } 158 | } 159 | 160 | return $jsonSchema; 161 | } 162 | 163 | /** 164 | * {@inheritdoc} 165 | */ 166 | public function retrieve($uri, $baseUri = null, $translate = true) 167 | { 168 | $resolver = new UriResolver(); 169 | $resolvedUri = $fetchUri = $resolver->resolve($uri, $baseUri); 170 | 171 | //fetch URL without #fragment 172 | $arParts = $resolver->parse($resolvedUri); 173 | if (isset($arParts['fragment'])) { 174 | unset($arParts['fragment']); 175 | $fetchUri = $resolver->generate($arParts); 176 | } 177 | 178 | // apply URI translations 179 | if ($translate) { 180 | $fetchUri = $this->translate($fetchUri); 181 | } 182 | 183 | $jsonSchema = $this->loadSchema($fetchUri); 184 | 185 | // Use the JSON pointer if specified 186 | $jsonSchema = $this->resolvePointer($jsonSchema, $resolvedUri); 187 | 188 | if ($jsonSchema instanceof \stdClass) { 189 | $jsonSchema->id = $resolvedUri; 190 | } 191 | 192 | return $jsonSchema; 193 | } 194 | 195 | /** 196 | * Fetch a schema from the given URI, json-decode it and return it. 197 | * Caches schema objects. 198 | * 199 | * @param string $fetchUri Absolute URI 200 | * 201 | * @return object JSON schema object 202 | */ 203 | protected function loadSchema($fetchUri) 204 | { 205 | if (isset($this->schemaCache[$fetchUri])) { 206 | return $this->schemaCache[$fetchUri]; 207 | } 208 | 209 | $uriRetriever = $this->getUriRetriever(); 210 | $contents = $this->uriRetriever->retrieve($fetchUri); 211 | $this->confirmMediaType($uriRetriever, $fetchUri); 212 | $jsonSchema = json_decode($contents); 213 | 214 | if (JSON_ERROR_NONE < $error = json_last_error()) { 215 | throw new JsonDecodingException($error); 216 | } 217 | 218 | $this->schemaCache[$fetchUri] = $jsonSchema; 219 | 220 | return $jsonSchema; 221 | } 222 | 223 | /** 224 | * Set the URI Retriever 225 | * 226 | * @param UriRetrieverInterface $uriRetriever 227 | * 228 | * @return $this for chaining 229 | */ 230 | public function setUriRetriever(UriRetrieverInterface $uriRetriever) 231 | { 232 | $this->uriRetriever = $uriRetriever; 233 | 234 | return $this; 235 | } 236 | 237 | /** 238 | * Parses a URI into five main components 239 | * 240 | * @param string $uri 241 | * 242 | * @return array 243 | */ 244 | public function parse($uri) 245 | { 246 | preg_match('|^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?|', $uri, $match); 247 | 248 | $components = []; 249 | if (5 < count($match)) { 250 | $components = [ 251 | 'scheme' => $match[2], 252 | 'authority' => $match[4], 253 | 'path' => $match[5] 254 | ]; 255 | } 256 | 257 | if (7 < count($match)) { 258 | $components['query'] = $match[7]; 259 | } 260 | 261 | if (9 < count($match)) { 262 | $components['fragment'] = $match[9]; 263 | } 264 | 265 | return $components; 266 | } 267 | 268 | /** 269 | * Builds a URI based on n array with the main components 270 | * 271 | * @param array $components 272 | * 273 | * @return string 274 | */ 275 | public function generate(array $components) 276 | { 277 | $uri = $components['scheme'] . '://' 278 | . $components['authority'] 279 | . $components['path']; 280 | 281 | if (array_key_exists('query', $components)) { 282 | $uri .= $components['query']; 283 | } 284 | 285 | if (array_key_exists('fragment', $components)) { 286 | $uri .= $components['fragment']; 287 | } 288 | 289 | return $uri; 290 | } 291 | 292 | /** 293 | * Resolves a URI 294 | * 295 | * @param string $uri Absolute or relative 296 | * @param string $baseUri Optional base URI 297 | * 298 | * @return string 299 | */ 300 | public function resolve($uri, $baseUri = null) 301 | { 302 | $components = $this->parse($uri); 303 | $path = $components['path']; 304 | 305 | if ((array_key_exists('scheme', $components)) && ('http' === $components['scheme'])) { 306 | return $uri; 307 | } 308 | 309 | $baseComponents = $this->parse($baseUri); 310 | $basePath = $baseComponents['path']; 311 | 312 | $baseComponents['path'] = UriResolver::combineRelativePathWithBasePath($path, $basePath); 313 | 314 | return $this->generate($baseComponents); 315 | } 316 | 317 | /** 318 | * @param string $uri 319 | * 320 | * @return bool 321 | */ 322 | public function isValid($uri) 323 | { 324 | $components = $this->parse($uri); 325 | 326 | return !empty($components); 327 | } 328 | 329 | /** 330 | * Set a URL translation rule 331 | */ 332 | public function setTranslation($from, $to) 333 | { 334 | $this->translationMap[$from] = $to; 335 | } 336 | 337 | /** 338 | * Apply URI translation rules 339 | */ 340 | public function translate($uri) 341 | { 342 | foreach ($this->translationMap as $from => $to) { 343 | $uri = preg_replace($from, $to, $uri); 344 | } 345 | 346 | // translate references to local files within the json-schema package 347 | $uri = preg_replace('|^package://|', sprintf('file://%s/', realpath(__DIR__ . '/../../..')), $uri); 348 | 349 | return $uri; 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /src/JsonSchema/UriResolverInterface.php: -------------------------------------------------------------------------------- 1 | 22 | * @author Bruno Prieto Reis 23 | * 24 | * @see README.md 25 | */ 26 | class Validator extends BaseConstraint 27 | { 28 | public const SCHEMA_MEDIA_TYPE = 'application/schema+json'; 29 | 30 | public const ERROR_NONE = 0; 31 | public const ERROR_ALL = -1; 32 | public const ERROR_DOCUMENT_VALIDATION = 1; 33 | public const ERROR_SCHEMA_VALIDATION = 2; 34 | 35 | /** 36 | * Validates the given data against the schema and returns an object containing the results 37 | * Both the php object and the schema are supposed to be a result of a json_decode call. 38 | * The validation works as defined by the schema proposal in http://json-schema.org. 39 | * 40 | * Note that the first argument is passed by reference, so you must pass in a variable. 41 | * 42 | * @param mixed $value 43 | * @param mixed $schema 44 | * 45 | * @phpstan-param int-mask-of $checkMode 46 | * @phpstan-return int-mask-of 47 | */ 48 | public function validate(&$value, $schema = null, ?int $checkMode = null): int 49 | { 50 | // reset errors prior to validation 51 | $this->reset(); 52 | 53 | // set checkMode 54 | $initialCheckMode = $this->factory->getConfig(); 55 | if ($checkMode !== null) { 56 | $this->factory->setConfig($checkMode); 57 | } 58 | 59 | // add provided schema to SchemaStorage with internal URI to allow internal $ref resolution 60 | $schemaURI = SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI; 61 | if (LooseTypeCheck::propertyExists($schema, 'id')) { 62 | $schemaURI = LooseTypeCheck::propertyGet($schema, 'id'); 63 | } 64 | $this->factory->getSchemaStorage()->addSchema($schemaURI, $schema); 65 | 66 | $validator = $this->factory->createInstanceFor('schema'); 67 | $validator->check( 68 | $value, 69 | $this->factory->getSchemaStorage()->getSchema($schemaURI) 70 | ); 71 | 72 | $this->factory->setConfig($initialCheckMode); 73 | 74 | $this->addErrors(array_unique($validator->getErrors(), SORT_REGULAR)); 75 | 76 | return $validator->getErrorMask(); 77 | } 78 | 79 | /** 80 | * Alias to validate(), to maintain backwards-compatibility with the previous API 81 | * 82 | * @deprecated since 6.0.0, use Validator::validate() instead, to be removed in 7.0 83 | * 84 | * @param mixed $value 85 | * @param mixed $schema 86 | * 87 | * @phpstan-return int-mask-of 88 | */ 89 | public function check($value, $schema): int 90 | { 91 | return $this->validate($value, $schema); 92 | } 93 | 94 | /** 95 | * Alias to validate(), to maintain backwards-compatibility with the previous API 96 | * 97 | * @deprecated since 6.0.0, use Validator::validate() instead, to be removed in 7.0 98 | * 99 | * @param mixed $value 100 | * @param mixed $schema 101 | * 102 | * @phpstan-return int-mask-of 103 | */ 104 | public function coerce(&$value, $schema): int 105 | { 106 | return $this->validate($value, $schema, Constraint::CHECK_MODE_COERCE_TYPES); 107 | } 108 | } 109 | --------------------------------------------------------------------------------