├── .gitignore ├── tests ├── 01 - pure validation │ ├── 00 - API │ │ ├── 02 - isValid.json │ │ └── 01 - validate.json │ ├── 06 - composite │ │ ├── 04 - not.json │ │ ├── 01 - allOf.json │ │ ├── 02 - anyOf.json │ │ └── 03 - oneOf.json │ ├── 03 - array constraints │ │ ├── 04 -unique.json │ │ ├── 03 - length.json │ │ ├── 01 - items.json │ │ └── 02 - additionalItems.json │ ├── 02 - object constraints │ │ ├── 02 -patternProperties.json │ │ ├── 04 - required.json │ │ ├── 01 - properties.json │ │ ├── 06 - min-max properties.json │ │ ├── 03 - additionalProperties.json │ │ └── 05 - dependencies.json │ ├── 04 - string constraints │ │ ├── 02 - pattern.json │ │ └── 01 - length.json │ ├── 01 - basic constraints │ │ ├── types │ │ │ ├── 01 - passes all types by default.json │ │ │ ├── 09 - multiple types.json │ │ │ ├── 08 - null.json │ │ │ ├── 03 - array.json │ │ │ ├── 02 - object.json │ │ │ ├── 04 - string.json │ │ │ ├── 07 - boolean.json │ │ │ ├── 05 - number.json │ │ │ └── 06- integer.json │ │ └── 01 - enum.json │ └── 05 - number constraints │ │ ├── 02 - minimum.json │ │ ├── 03 - maximum.json │ │ └── 01 - multipleOf.json ├── 02 - coercive validation │ ├── 00 - API │ │ └── 01 - coerce.json │ ├── 02 - missing properties.json │ └── 01 - simple type juggling.json └── 03 - schema store │ ├── 01 - fetch and retrieve.php │ ├── 02 - add using id.php │ └── 03 - references.php ├── LICENSE.txt ├── composer.json ├── src └── Jsv4 │ ├── ValidationException.php │ ├── SchemaStore.php │ └── Validator.php ├── LICENSE-MIT.txt ├── test-utils.php ├── README.md └── test.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | nbproject 4 | vendor -------------------------------------------------------------------------------- /tests/01 - pure validation/00 - API/02 - isValid.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "isValid", 3 | "schema": {}, 4 | "data": { 5 | "foo": "bar" 6 | }, 7 | "result": true 8 | } -------------------------------------------------------------------------------- /tests/01 - pure validation/00 - API/01 - validate.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "validate", 3 | "schema": {}, 4 | "data": { 5 | "foo": "bar" 6 | }, 7 | "result": { 8 | "/valid": true 9 | } 10 | } -------------------------------------------------------------------------------- /tests/02 - coercive validation/00 - API/01 - coerce.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "coerce", 3 | "schema": {}, 4 | "data": { 5 | "foo": "bar" 6 | }, 7 | "result": { 8 | "/valid": true, 9 | "/value": {"foo": "bar"} 10 | } 11 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Author: Geraint Luff 2 | Year: 2013 3 | 4 | This code is released into the "public domain" by its author(s). Anybody may use, alter and distribute the code without restriction. The author makes no guarantees, and takes no liability of any kind for use of this code. 5 | 6 | If you find a bug or make an improvement, it would be courteous to let the author know, but it is not compulsory. 7 | -------------------------------------------------------------------------------- /tests/03 - schema store/01 - fetch and retrieve.php: -------------------------------------------------------------------------------- 1 | add($url, $schema); 13 | 14 | if (!recursiveEqual($store->get($url), $schema)) { 15 | throw new Exception("Not equal"); 16 | } 17 | if (!recursiveEqual($store->get($url . "#/title"), $schema->title)) { 18 | throw new Exception("Not equal"); 19 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geraintluff/jsv4", 3 | "description": "A (coercive) JSON Schema v4 Validator for PHP", 4 | "keywords": ["JSON Schema", "JSON", "schema", "validator", "v4", "Geraint Luff", "geraintluff"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Geraint Luff", 9 | "email": "luffgd@gmail.com" 10 | }, 11 | { 12 | "name": "Martin Bažík", 13 | "email": "martin@bazo.sk", 14 | "homepage": "http://bazo.sk" 15 | } 16 | ], 17 | "require": { 18 | "php": ">=5.4.0" 19 | }, 20 | "autoload": { 21 | "psr-0": { 22 | "Jsv4": "src/" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/01 - pure validation/06 - composite/04 - not.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "not passed", 5 | "schema": { 6 | "not": {"type": "integer"} 7 | }, 8 | "data": 4.5, 9 | "result": { 10 | "/valid": true 11 | } 12 | }, 13 | { 14 | "method": "validate", 15 | "title": "not failed", 16 | "schema": { 17 | "not": {"type": "integer"} 18 | }, 19 | "data": 18, 20 | "result": { 21 | "/valid": false, 22 | "/errors/0/code": 13, 23 | "/errors/0/dataPath": "", 24 | "/errors/0/schemaPath": "/not", 25 | "/errors/0/message": "Value satisfies prohibited schema" 26 | } 27 | } 28 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/06 - composite/01 - allOf.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "allOf type constraint passed", 5 | "schema": { 6 | "allOf": [ 7 | {"type": "number"} 8 | ] 9 | }, 10 | "data": 4.5, 11 | "result": { 12 | "/valid": true 13 | } 14 | }, 15 | { 16 | "method": "validate", 17 | "title": "allOf type constraint passed", 18 | "schema": { 19 | "allOf": [ 20 | {"type": "number"} 21 | ] 22 | }, 23 | "data": "string value", 24 | "result": { 25 | "/valid": false, 26 | "/errors/0/code": 0, 27 | "/errors/0/dataPath": "", 28 | "/errors/0/schemaPath": "/allOf/0/type" 29 | } 30 | } 31 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/03 - array constraints/04 -unique.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "uniqueItems passes", 5 | "schema": { 6 | "uniqueItems": true 7 | }, 8 | "data": [true, false, {"foo": "bar"}, {"foo": "not-bar"}], 9 | "result": { 10 | "/valid": true 11 | } 12 | }, 13 | { 14 | "method": "validate", 15 | "title": "uniqueItems fails", 16 | "schema": { 17 | "uniqueItems": true 18 | }, 19 | "data": [true, false, {"foo": "bar"}, {"foo": "bar"}], 20 | "result": { 21 | "/valid": false, 22 | "/errors/0/code": 402, 23 | "/errors/0/dataPath": "", 24 | "/errors/0/schemaPath": "/uniqueItems", 25 | "/errors/0/message": "Array items must be unique (items 2 and 3)" 26 | } 27 | } 28 | ] -------------------------------------------------------------------------------- /src/Jsv4/ValidationException.php: -------------------------------------------------------------------------------- 1 | code = $code; 18 | $this->dataPath = $dataPath; 19 | $this->schemaPath = $schemaPath; 20 | $this->message = $errorMessage; 21 | if ($subResults) { 22 | $this->subResults = $subResults; 23 | } 24 | } 25 | 26 | 27 | public function prefix($dataPrefix, $schemaPrefix) 28 | { 29 | return new ValidationException($this->code, $dataPrefix . $this->dataPath, $schemaPrefix . $this->schemaPath, $this->message); 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /tests/01 - pure validation/02 - object constraints/02 -patternProperties.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "patternProperties check passes", 5 | "schema": { 6 | "patternProperties": { 7 | "^int": {"type": "integer"}, 8 | "^string": {"type": "string"} 9 | } 10 | }, 11 | "data": { 12 | "intFive": 5, 13 | "string5": "five" 14 | }, 15 | "result": { 16 | "/valid": true 17 | } 18 | }, 19 | { 20 | "method": "validate", 21 | "title": "patternProperties check fails", 22 | "schema": { 23 | "patternProperties": { 24 | "^int": {"type": "integer"}, 25 | "^string": {"type": "string"} 26 | } 27 | }, 28 | "data": { 29 | "intFive": "five" 30 | }, 31 | "result": { 32 | "/valid": false, 33 | "/errors/0/code": 0, 34 | "/errors/0/dataPath": "/intFive", 35 | "/errors/0/schemaPath": "/patternProperties/^int/type" 36 | } 37 | } 38 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/04 - string constraints/02 - pattern.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "pattern passes", 5 | "schema": { 6 | "pattern": "[0-9]/[a-zA-Z]*" 7 | }, 8 | "data": "5/blah", 9 | "result": { 10 | "/valid": true 11 | } 12 | }, 13 | { 14 | "method": "validate", 15 | "title": "pattern fails", 16 | "schema": { 17 | "pattern": "[0-9]/[a-zA-Z]*" 18 | }, 19 | "data": "5-blah", 20 | "result": { 21 | "/valid": false, 22 | "/errors/0/code": 202, 23 | "/errors/0/dataPath": "", 24 | "/errors/0/schemaPath": "/pattern", 25 | "/errors/0/message": "String does not match pattern: [0-9]/[a-zA-Z]*" 26 | } 27 | }, 28 | { 29 | "method": "validate", 30 | "title": "pattern is not anchored", 31 | "schema": { 32 | "pattern": "[0-9]/[a-zA-Z]*" 33 | }, 34 | "data": "prefix 5/blah suffix", 35 | "result": { 36 | "/valid": true 37 | } 38 | } 39 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/02 - object constraints/04 - required.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "required passes when properties exist", 5 | "schema": { 6 | "required": ["foo", "bar"] 7 | }, 8 | "data": { 9 | "foo": true, 10 | "bar": false 11 | }, 12 | "result": { 13 | "/valid": true 14 | } 15 | }, 16 | { 17 | "method": "validate", 18 | "title": "required fails when properties missing", 19 | "schema": { 20 | "required": ["foo", "bar"] 21 | }, 22 | "data": { 23 | "foo": true 24 | }, 25 | "result": { 26 | "/valid": false, 27 | "/errors/0/code": 302, 28 | "/errors/0/dataPath": "", 29 | "/errors/0/schemaPath": "/required/1", 30 | "/errors/0/message": "Missing required property: bar" 31 | } 32 | }, 33 | { 34 | "method": "validate", 35 | "title": "required passes for non-objects", 36 | "schema": { 37 | "required": ["foo", "bar"] 38 | }, 39 | "data": "string", 40 | "result": { 41 | "/valid": true 42 | } 43 | } 44 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/06 - composite/02 - anyOf.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "anyOf passed", 5 | "schema": { 6 | "anyOf": [ 7 | {"type": "number"}, 8 | {"enum": [4, "four"]} 9 | ] 10 | }, 11 | "data": "four", 12 | "result": { 13 | "/valid": true 14 | } 15 | }, 16 | { 17 | "method": "validate", 18 | "title": "anyOf passed multiple", 19 | "schema": { 20 | "anyOf": [ 21 | {"type": "number"}, 22 | {"enum": [4, "four"]} 23 | ] 24 | }, 25 | "data": 4, 26 | "result": { 27 | "/valid": true 28 | } 29 | }, 30 | { 31 | "method": "validate", 32 | "title": "anyOf failed", 33 | "schema": { 34 | "anyOf": [ 35 | {"type": "number"}, 36 | {"enum": [4, "four"]} 37 | ] 38 | }, 39 | "data": false, 40 | "result": { 41 | "/valid": false, 42 | "/errors/0/code": 10, 43 | "/errors/0/dataPath": "", 44 | "/errors/0/schemaPath": "/anyOf", 45 | "/errors/0/message": "Value must satisfy at least one of the options" 46 | } 47 | } 48 | ] -------------------------------------------------------------------------------- /LICENSE-MIT.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Geraint Luff 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /tests/01 - pure validation/06 - composite/03 - oneOf.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "oneOf passed", 5 | "schema": { 6 | "oneOf": [ 7 | {"type": "number"}, 8 | {"enum": [4, "four"]} 9 | ] 10 | }, 11 | "data": "four", 12 | "result": { 13 | "/valid": true 14 | } 15 | }, 16 | { 17 | "method": "validate", 18 | "title": "oneOf failed multiple", 19 | "schema": { 20 | "oneOf": [ 21 | {"type": "number"}, 22 | {"enum": [4, "four"]} 23 | ] 24 | }, 25 | "data": 4, 26 | "result": { 27 | "/valid": false, 28 | "/errors/0/code": 12, 29 | "/errors/0/dataPath": "", 30 | "/errors/0/schemaPath": "/oneOf", 31 | "/errors/0/message": "Value satisfies more than one of the options (0 and 1)" 32 | } 33 | }, 34 | { 35 | "method": "validate", 36 | "title": "oneOf failed", 37 | "schema": { 38 | "oneOf": [ 39 | {"type": "number"}, 40 | {"enum": [4, "four"]} 41 | ] 42 | }, 43 | "data": false, 44 | "result": { 45 | "/valid": false, 46 | "/errors/0/code": 11, 47 | "/errors/0/dataPath": "", 48 | "/errors/0/schemaPath": "/oneOf", 49 | "/errors/0/message": "Value must satisfy one of the options" 50 | } 51 | } 52 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/01 - basic constraints/types/01 - passes all types by default.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "Empty schema passes object", 5 | "schema": {}, 6 | "data": { 7 | "foo": "bar" 8 | }, 9 | "result": { 10 | "/valid": true 11 | } 12 | }, 13 | { 14 | "method": "validate", 15 | "title": "Empty schema passes array", 16 | "schema": {}, 17 | "data": ["foo", true], 18 | "result": { 19 | "/valid": true 20 | } 21 | }, 22 | { 23 | "method": "validate", 24 | "title": "Empty schema passes string", 25 | "schema": {}, 26 | "data": "testing, testing...", 27 | "result": { 28 | "/valid": true 29 | } 30 | }, 31 | { 32 | "method": "validate", 33 | "title": "Empty schema passes number", 34 | "schema": {}, 35 | "data": 15.52, 36 | "result": { 37 | "/valid": true 38 | } 39 | }, 40 | { 41 | "method": "validate", 42 | "title": "Empty schema passes boolean", 43 | "schema": {}, 44 | "data": true, 45 | "result": { 46 | "/valid": true 47 | } 48 | }, 49 | { 50 | "method": "validate", 51 | "title": "Empty schema passes null", 52 | "schema": {}, 53 | "data": null, 54 | "result": { 55 | "/valid": true 56 | } 57 | } 58 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/03 - array constraints/03 - length.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "minItems passes", 5 | "schema": { 6 | "minItems": 1 7 | }, 8 | "data": [true], 9 | "result": { 10 | "/valid": true 11 | } 12 | }, 13 | { 14 | "method": "validate", 15 | "title": "minItems fails", 16 | "schema": { 17 | "minItems": 2 18 | }, 19 | "data": [true], 20 | "result": { 21 | "/valid": false, 22 | "/errors/0/code": 400, 23 | "/errors/0/dataPath": "", 24 | "/errors/0/schemaPath": "/minItems", 25 | "/errors/0/message": "Array is too short (must have at least 2 items)" 26 | } 27 | }, 28 | { 29 | "method": "validate", 30 | "title": "maxItems passes", 31 | "schema": { 32 | "maxItems": 2 33 | }, 34 | "data": [true], 35 | "result": { 36 | "/valid": true 37 | } 38 | }, 39 | { 40 | "method": "validate", 41 | "title": "maxItems fails", 42 | "schema": { 43 | "maxItems": 2 44 | }, 45 | "data": [true, 1, "one"], 46 | "result": { 47 | "/valid": false, 48 | "/errors/0/code": 401, 49 | "/errors/0/dataPath": "", 50 | "/errors/0/schemaPath": "/maxItems", 51 | "/errors/0/message": "Array is too long (must have at most 2 items)" 52 | } 53 | } 54 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/05 - number constraints/02 - minimum.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "minimum passed", 5 | "schema": { 6 | "minimum": 1.5 7 | }, 8 | "data": 1.5, 9 | "result": { 10 | "/valid": true 11 | } 12 | }, 13 | { 14 | "method": "validate", 15 | "title": "exclusive minimum passed", 16 | "schema": { 17 | "minimum": 1.5, 18 | "exclusiveMinimum": true 19 | }, 20 | "data": 4.5, 21 | "result": { 22 | "/valid": true 23 | } 24 | }, 25 | { 26 | "method": "validate", 27 | "title": "minimum failed", 28 | "schema": { 29 | "minimum": 1.5 30 | }, 31 | "data": 1, 32 | "result": { 33 | "/valid": false, 34 | "/errors/0/code": 101, 35 | "/errors/0/dataPath": "", 36 | "/errors/0/schemaPath": "/minimum", 37 | "/errors/0/message": "Number must be >= 1.5" 38 | } 39 | }, 40 | { 41 | "method": "validate", 42 | "title": "exclusive minimum failed", 43 | "schema": { 44 | "minimum": 1.5, 45 | "exclusiveMinimum": true 46 | }, 47 | "data": 1.5, 48 | "result": { 49 | "/valid": false, 50 | "/errors/0/code": 102, 51 | "/errors/0/dataPath": "", 52 | "/errors/0/schemaPath": "", 53 | "/errors/0/message": "Number must be > 1.5" 54 | } 55 | } 56 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/05 - number constraints/03 - maximum.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "maximum passed", 5 | "schema": { 6 | "maximum": 1.5 7 | }, 8 | "data": 1.5, 9 | "result": { 10 | "/valid": true 11 | } 12 | }, 13 | { 14 | "method": "validate", 15 | "title": "exclusive maximum passed", 16 | "schema": { 17 | "maximum": 1.5, 18 | "exclusiveMaximum": true 19 | }, 20 | "data": 0.5, 21 | "result": { 22 | "/valid": true 23 | } 24 | }, 25 | { 26 | "method": "validate", 27 | "title": "maximum failed", 28 | "schema": { 29 | "maximum": 1.5 30 | }, 31 | "data": 2, 32 | "result": { 33 | "/valid": false, 34 | "/errors/0/code": 103, 35 | "/errors/0/dataPath": "", 36 | "/errors/0/schemaPath": "/maximum", 37 | "/errors/0/message": "Number must be <= 1.5" 38 | } 39 | }, 40 | { 41 | "method": "validate", 42 | "title": "exclusive maximum failed", 43 | "schema": { 44 | "maximum": 1.5, 45 | "exclusiveMaximum": true 46 | }, 47 | "data": 1.5, 48 | "result": { 49 | "/valid": false, 50 | "/errors/0/code": 104, 51 | "/errors/0/dataPath": "", 52 | "/errors/0/schemaPath": "", 53 | "/errors/0/message": "Number must be < 1.5" 54 | } 55 | } 56 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/04 - string constraints/01 - length.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "minLength passed", 5 | "schema": { 6 | "minLength": 5 7 | }, 8 | "data": "blah blah blah", 9 | "result": { 10 | "/valid": true 11 | } 12 | }, 13 | { 14 | "method": "validate", 15 | "title": "minLength failed", 16 | "schema": { 17 | "minLength": 5 18 | }, 19 | "data": "blah", 20 | "result": { 21 | "/valid": false, 22 | "/errors/0/code": 200, 23 | "/errors/0/dataPath": "", 24 | "/errors/0/schemaPath": "/minLength", 25 | "/errors/0/message": "String must be at least 5 characters long" 26 | } 27 | }, 28 | { 29 | "method": "validate", 30 | "title": "maxLength passed", 31 | "schema": { 32 | "maxLength": 5 33 | }, 34 | "data": "blah", 35 | "result": { 36 | "/valid": true 37 | } 38 | }, 39 | { 40 | "method": "validate", 41 | "title": "maxLength failed", 42 | "schema": { 43 | "maxLength": 5 44 | }, 45 | "data": "blah blah blah", 46 | "result": { 47 | "/valid": false, 48 | "/errors/0/code": 201, 49 | "/errors/0/dataPath": "", 50 | "/errors/0/schemaPath": "/maxLength", 51 | "/errors/0/message": "String must be at most 5 characters long" 52 | } 53 | } 54 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/05 - number constraints/01 - multipleOf.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "multipleOf passed", 5 | "schema": { 6 | "multipleOf": 1.5 7 | }, 8 | "data": 4.5, 9 | "result": { 10 | "/valid": true 11 | } 12 | }, 13 | { 14 | "method": "validate", 15 | "title": "multipleOf failed", 16 | "schema": { 17 | "multipleOf": 1.5 18 | }, 19 | "data": 5.5, 20 | "result": { 21 | "/valid": false, 22 | "/errors/0/code": 100, 23 | "/errors/0/dataPath": "", 24 | "/errors/0/schemaPath": "/multipleOf", 25 | "/errors/0/message": "Number must be a multiple of 1.5" 26 | } 27 | }, 28 | { 29 | "method": "validate", 30 | "title": "small number multipleOf passed", 31 | "schema": { 32 | "multipleOf": 0.0001 33 | }, 34 | "data": 0.0075, 35 | "result": { 36 | "/valid": true 37 | } 38 | }, 39 | { 40 | "method": "validate", 41 | "title": "small number multipleOf failed", 42 | "schema": { 43 | "multipleOf": 0.0001 44 | }, 45 | "data": 0.00751, 46 | "result": { 47 | "/valid": false, 48 | "/errors/0/code": 100, 49 | "/errors/0/dataPath": "", 50 | "/errors/0/schemaPath": "/multipleOf", 51 | "/errors/0/message": "Number must be a multiple of 0.0001" 52 | } 53 | } 54 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/01 - basic constraints/types/09 - multiple types.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "Array passes array", 5 | "schema": {"type": ["array"]}, 6 | "data": [true], 7 | "result": { 8 | "/valid": true 9 | } 10 | }, 11 | { 12 | "method": "validate", 13 | "title": "Array fails null", 14 | "schema": {"type": ["array"]}, 15 | "data": null, 16 | "result": { 17 | "/valid": false, 18 | "/errors/0/code": 0, 19 | "/errors/0/dataPath": "", 20 | "/errors/0/schemaPath": "/type", 21 | "/errors/0/message": "Invalid type: null" 22 | } 23 | }, 24 | { 25 | "method": "validate", 26 | "title": "Object/boolean passes object", 27 | "schema": {"type": ["object", "boolean"]}, 28 | "data": {}, 29 | "result": { 30 | "/valid": true 31 | } 32 | }, 33 | { 34 | "method": "validate", 35 | "title": "Object/boolean passes boolean", 36 | "schema": {"type": ["object", "boolean"]}, 37 | "data": true, 38 | "result": { 39 | "/valid": true 40 | } 41 | }, 42 | { 43 | "method": "validate", 44 | "title": "Object/boolean fails null", 45 | "schema": {"type": ["object", "boolean"]}, 46 | "data": null, 47 | "result": { 48 | "/valid": false, 49 | "/errors/0/code": 0, 50 | "/errors/0/dataPath": "", 51 | "/errors/0/schemaPath": "/type", 52 | "/errors/0/message": "Invalid type: null" 53 | } 54 | } 55 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/02 - object constraints/01 - properties.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "Property type-check passes", 5 | "schema": { 6 | "properties": { 7 | "foo": { 8 | "type": "string" 9 | } 10 | } 11 | }, 12 | "data": { 13 | "foo": "bar" 14 | }, 15 | "result": { 16 | "/valid": true 17 | } 18 | }, 19 | { 20 | "method": "validate", 21 | "title": "Property type-check fails", 22 | "schema": { 23 | "properties": { 24 | "foo": { 25 | "type": "string" 26 | } 27 | } 28 | }, 29 | "data": { 30 | "foo": false 31 | }, 32 | "result": { 33 | "/valid": false, 34 | "/errors/0/code": 0, 35 | "/errors/0/dataPath":"/foo", 36 | "/errors/0/schemaPath": "/properties/foo/type" 37 | } 38 | }, 39 | { 40 | "method": "validate", 41 | "title": "Property type-check passes for non-objects", 42 | "schema": { 43 | "properties": { 44 | "foo": { 45 | "type": "string" 46 | } 47 | } 48 | }, 49 | "data": [":)"], 50 | "result": { 51 | "/valid": true 52 | } 53 | }, 54 | { 55 | "method": "validate", 56 | "title": "Property type-check passes for missing property", 57 | "schema": { 58 | "properties": { 59 | "bar": { 60 | "type": "string" 61 | } 62 | } 63 | }, 64 | "data": { 65 | "foo": "text here" 66 | }, 67 | "result": { 68 | "/valid": true 69 | } 70 | }, 71 | { 72 | "method": "validate", 73 | "title": "Property type-check fails for null", 74 | "schema": { 75 | "properties": { 76 | "foo": { 77 | "type": "string" 78 | } 79 | } 80 | }, 81 | "data": { 82 | "foo": null 83 | }, 84 | "result": { 85 | "/valid": false 86 | } 87 | } 88 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/01 - basic constraints/types/08 - null.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "Fails object", 5 | "schema": {"type": "null"}, 6 | "data": { 7 | "foo": "bar" 8 | }, 9 | "result": { 10 | "/valid": false, 11 | "/errors/0/code": 0, 12 | "/errors/0/dataPath": "", 13 | "/errors/0/schemaPath": "/type", 14 | "/errors/0/message": "Invalid type: object" 15 | } 16 | }, 17 | { 18 | "method": "validate", 19 | "title": "Passes array", 20 | "schema": {"type": "null"}, 21 | "data": [null], 22 | "result": { 23 | "/valid": false, 24 | "/errors/0/code": 0, 25 | "/errors/0/dataPath": "", 26 | "/errors/0/schemaPath": "/type", 27 | "/errors/0/message": "Invalid type: array" 28 | } 29 | }, 30 | { 31 | "method": "validate", 32 | "title": "Fails string", 33 | "schema": {"type": "null"}, 34 | "data": "test", 35 | "result": { 36 | "/valid": false, 37 | "/errors/0/code": 0, 38 | "/errors/0/dataPath": "", 39 | "/errors/0/schemaPath": "/type", 40 | "/errors/0/message": "Invalid type: string" 41 | } 42 | }, 43 | { 44 | "method": "validate", 45 | "title": "Fails number", 46 | "schema": {"type": "null"}, 47 | "data": -5.2, 48 | "result": { 49 | "/valid": false, 50 | "/errors/0/code": 0, 51 | "/errors/0/dataPath": "", 52 | "/errors/0/schemaPath": "/type", 53 | "/errors/0/message": "Invalid type: number" 54 | } 55 | }, 56 | { 57 | "method": "validate", 58 | "title": "Fails boolean", 59 | "schema": {"type": "null"}, 60 | "data": false, 61 | "result": { 62 | "/valid": false, 63 | "/errors/0/code": 0, 64 | "/errors/0/dataPath": "", 65 | "/errors/0/schemaPath": "/type", 66 | "/errors/0/message": "Invalid type: boolean" 67 | } 68 | }, 69 | { 70 | "method": "validate", 71 | "title": "Fails null", 72 | "schema": {"type": "null"}, 73 | "data": null, 74 | "result": { 75 | "/valid": true 76 | } 77 | } 78 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/01 - basic constraints/types/03 - array.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "Fails object", 5 | "schema": {"type": "array"}, 6 | "data": { 7 | "foo": "bar" 8 | }, 9 | "result": { 10 | "/valid": false, 11 | "/errors/0/code": 0, 12 | "/errors/0/dataPath": "", 13 | "/errors/0/schemaPath": "/type", 14 | "/errors/0/message": "Invalid type: object" 15 | } 16 | }, 17 | { 18 | "method": "validate", 19 | "title": "Passes array", 20 | "schema": {"type": "array"}, 21 | "data": [null], 22 | "result": { 23 | "/valid": true 24 | } 25 | }, 26 | { 27 | "method": "validate", 28 | "title": "Fails string", 29 | "schema": {"type": "array"}, 30 | "data": "test", 31 | "result": { 32 | "/valid": false, 33 | "/errors/0/code": 0, 34 | "/errors/0/dataPath": "", 35 | "/errors/0/schemaPath": "/type", 36 | "/errors/0/message": "Invalid type: string" 37 | } 38 | }, 39 | { 40 | "method": "validate", 41 | "title": "Fails number", 42 | "schema": {"type": "array"}, 43 | "data": -0.001, 44 | "result": { 45 | "/valid": false, 46 | "/errors/0/code": 0, 47 | "/errors/0/dataPath": "", 48 | "/errors/0/schemaPath": "/type", 49 | "/errors/0/message": "Invalid type: number" 50 | } 51 | }, 52 | { 53 | "method": "validate", 54 | "title": "Fails boolean", 55 | "schema": {"type": "array"}, 56 | "data": false, 57 | "result": { 58 | "/valid": false, 59 | "/errors/0/code": 0, 60 | "/errors/0/dataPath": "", 61 | "/errors/0/schemaPath": "/type", 62 | "/errors/0/message": "Invalid type: boolean" 63 | } 64 | }, 65 | { 66 | "method": "validate", 67 | "title": "Fails null", 68 | "schema": {"type": "array"}, 69 | "data": null, 70 | "result": { 71 | "/valid": false, 72 | "/errors/0/code": 0, 73 | "/errors/0/dataPath": "", 74 | "/errors/0/schemaPath": "/type", 75 | "/errors/0/message": "Invalid type: null" 76 | } 77 | } 78 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/01 - basic constraints/types/02 - object.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "Passes object", 5 | "schema": {"type": "object"}, 6 | "data": { 7 | "foo": "bar" 8 | }, 9 | "result": { 10 | "/valid": true 11 | } 12 | }, 13 | { 14 | "method": "validate", 15 | "title": "Fails array", 16 | "schema": {"type": "object"}, 17 | "data": [null], 18 | "result": { 19 | "/valid": false, 20 | "/errors/0/code": 0, 21 | "/errors/0/dataPath": "", 22 | "/errors/0/schemaPath": "/type", 23 | "/errors/0/message": "Invalid type: array" 24 | } 25 | }, 26 | { 27 | "method": "validate", 28 | "title": "Fails string", 29 | "schema": {"type": "object"}, 30 | "data": "test", 31 | "result": { 32 | "/valid": false, 33 | "/errors/0/code": 0, 34 | "/errors/0/dataPath": "", 35 | "/errors/0/schemaPath": "/type", 36 | "/errors/0/message": "Invalid type: string" 37 | } 38 | }, 39 | { 40 | "method": "validate", 41 | "title": "Fails number", 42 | "schema": {"type": "object"}, 43 | "data": -0.001, 44 | "result": { 45 | "/valid": false, 46 | "/errors/0/code": 0, 47 | "/errors/0/dataPath": "", 48 | "/errors/0/schemaPath": "/type", 49 | "/errors/0/message": "Invalid type: number" 50 | } 51 | }, 52 | { 53 | "method": "validate", 54 | "title": "Fails boolean", 55 | "schema": {"type": "object"}, 56 | "data": false, 57 | "result": { 58 | "/valid": false, 59 | "/errors/0/code": 0, 60 | "/errors/0/dataPath": "", 61 | "/errors/0/schemaPath": "/type", 62 | "/errors/0/message": "Invalid type: boolean" 63 | } 64 | }, 65 | { 66 | "method": "validate", 67 | "title": "Fails null", 68 | "schema": {"type": "object"}, 69 | "data": null, 70 | "result": { 71 | "/valid": false, 72 | "/errors/0/code": 0, 73 | "/errors/0/dataPath": "", 74 | "/errors/0/schemaPath": "/type", 75 | "/errors/0/message": "Invalid type: null" 76 | } 77 | } 78 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/01 - basic constraints/types/04 - string.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "Fails object", 5 | "schema": {"type": "string"}, 6 | "data": { 7 | "foo": "bar" 8 | }, 9 | "result": { 10 | "/valid": false, 11 | "/errors/0/code": 0, 12 | "/errors/0/dataPath": "", 13 | "/errors/0/schemaPath": "/type", 14 | "/errors/0/message": "Invalid type: object" 15 | } 16 | }, 17 | { 18 | "method": "validate", 19 | "title": "Fails array", 20 | "schema": {"type": "string"}, 21 | "data": [null], 22 | "result": { 23 | "/valid": false, 24 | "/errors/0/code": 0, 25 | "/errors/0/dataPath": "", 26 | "/errors/0/schemaPath": "/type", 27 | "/errors/0/message": "Invalid type: array" 28 | } 29 | }, 30 | { 31 | "method": "validate", 32 | "title": "Passes string", 33 | "schema": {"type": "string"}, 34 | "data": "test", 35 | "result": { 36 | "/valid": true 37 | } 38 | }, 39 | { 40 | "method": "validate", 41 | "title": "Fails number", 42 | "schema": {"type": "string"}, 43 | "data": -0.001, 44 | "result": { 45 | "/valid": false, 46 | "/errors/0/code": 0, 47 | "/errors/0/dataPath": "", 48 | "/errors/0/schemaPath": "/type", 49 | "/errors/0/message": "Invalid type: number" 50 | } 51 | }, 52 | { 53 | "method": "validate", 54 | "title": "Fails boolean", 55 | "schema": {"type": "string"}, 56 | "data": false, 57 | "result": { 58 | "/valid": false, 59 | "/errors/0/code": 0, 60 | "/errors/0/dataPath": "", 61 | "/errors/0/schemaPath": "/type", 62 | "/errors/0/message": "Invalid type: boolean" 63 | } 64 | }, 65 | { 66 | "method": "validate", 67 | "title": "Fails null", 68 | "schema": {"type": "string"}, 69 | "data": null, 70 | "result": { 71 | "/valid": false, 72 | "/errors/0/code": 0, 73 | "/errors/0/dataPath": "", 74 | "/errors/0/schemaPath": "/type", 75 | "/errors/0/message": "Invalid type: null" 76 | } 77 | } 78 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/01 - basic constraints/types/07 - boolean.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "Fails object", 5 | "schema": {"type": "boolean"}, 6 | "data": { 7 | "foo": "bar" 8 | }, 9 | "result": { 10 | "/valid": false, 11 | "/errors/0/code": 0, 12 | "/errors/0/dataPath": "", 13 | "/errors/0/schemaPath": "/type", 14 | "/errors/0/message": "Invalid type: object" 15 | } 16 | }, 17 | { 18 | "method": "validate", 19 | "title": "Passes array", 20 | "schema": {"type": "boolean"}, 21 | "data": [null], 22 | "result": { 23 | "/valid": false, 24 | "/errors/0/code": 0, 25 | "/errors/0/dataPath": "", 26 | "/errors/0/schemaPath": "/type", 27 | "/errors/0/message": "Invalid type: array" 28 | } 29 | }, 30 | { 31 | "method": "validate", 32 | "title": "Fails string", 33 | "schema": {"type": "boolean"}, 34 | "data": "test", 35 | "result": { 36 | "/valid": false, 37 | "/errors/0/code": 0, 38 | "/errors/0/dataPath": "", 39 | "/errors/0/schemaPath": "/type", 40 | "/errors/0/message": "Invalid type: string" 41 | } 42 | }, 43 | { 44 | "method": "validate", 45 | "title": "Fails number", 46 | "schema": {"type": "boolean"}, 47 | "data": -5.2, 48 | "result": { 49 | "/valid": false, 50 | "/errors/0/code": 0, 51 | "/errors/0/dataPath": "", 52 | "/errors/0/schemaPath": "/type", 53 | "/errors/0/message": "Invalid type: number" 54 | } 55 | }, 56 | { 57 | "method": "validate", 58 | "title": "Passes boolean", 59 | "schema": {"type": "boolean"}, 60 | "data": false, 61 | "result": { 62 | "/valid": true 63 | } 64 | }, 65 | { 66 | "method": "validate", 67 | "title": "Fails null", 68 | "schema": {"type": "boolean"}, 69 | "data": null, 70 | "result": { 71 | "/valid": false, 72 | "/errors/0/code": 0, 73 | "/errors/0/dataPath": "", 74 | "/errors/0/schemaPath": "/type", 75 | "/errors/0/message": "Invalid type: null" 76 | } 77 | } 78 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/03 - array constraints/01 - items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "Items type-check passes for zero-length array", 5 | "schema": { 6 | "items": { 7 | "type": "boolean" 8 | } 9 | }, 10 | "data": [], 11 | "result": { 12 | "/valid": true 13 | } 14 | }, 15 | { 16 | "method": "validate", 17 | "title": "Items type-check passes for correct type", 18 | "schema": { 19 | "items": { 20 | "type": "boolean" 21 | } 22 | }, 23 | "data": [true], 24 | "result": { 25 | "/valid": true 26 | } 27 | }, 28 | { 29 | "method": "validate", 30 | "title": "Items type-check fails", 31 | "schema": { 32 | "items": { 33 | "type": "boolean" 34 | } 35 | }, 36 | "data": [5], 37 | "result": { 38 | "/valid": false, 39 | "/errors/0/code": 0, 40 | "/errors/0/dataPath": "/0", 41 | "/errors/0/schemaPath": "/items/type" 42 | } 43 | }, 44 | { 45 | "method": "validate", 46 | "title": "Tuple-typing type-check passes for single-length array", 47 | "schema": { 48 | "items": [ 49 | {"type": "boolean"}, 50 | {"type": "integer"} 51 | ] 52 | }, 53 | "data": [true], 54 | "result": { 55 | "/valid": true 56 | } 57 | }, 58 | { 59 | "method": "validate", 60 | "title": "Tuple-typing type-check fails for full-length array", 61 | "schema": { 62 | "items": [ 63 | {"type": "boolean"}, 64 | {"type": "integer"} 65 | ] 66 | }, 67 | "data": [true, "test"], 68 | "result": { 69 | "/valid": false, 70 | "/errors/0/code": 0, 71 | "/errors/0/dataPath": "/1", 72 | "/errors/0/schemaPath": "/items/1/type" 73 | } 74 | }, 75 | { 76 | "method": "validate", 77 | "title": "Tuple-typing type-check passes for over-length array", 78 | "schema": { 79 | "items": [ 80 | {"type": "boolean"}, 81 | {"type": "integer"} 82 | ] 83 | }, 84 | "data": [true, 5, "test"], 85 | "result": { 86 | "/valid": true 87 | } 88 | } 89 | ] -------------------------------------------------------------------------------- /tests/03 - schema store/02 - add using id.php: -------------------------------------------------------------------------------- 1 | add($url, $schema); 40 | 41 | if (!recursiveEqual($store->get($url . "#foo"), $schema->properties->foo)) { 42 | throw new Exception("#foo not found"); 43 | } 44 | 45 | if (!recursiveEqual($store->get($url . "?baz=1"), $schema->properties->baz)) { 46 | throw new Exception("?baz=1 not found"); 47 | } 48 | 49 | if (!recursiveEqual($store->get($url . "/foobar"), $schema->properties->foobar)) { 50 | throw new Exception("/foobar not found"); 51 | } 52 | 53 | if (!recursiveEqual($store->get($url . "/foo#bar"), $schema->properties->nestedSchema->nested)) { 54 | throw new Exception("/foo#bar not found"); 55 | } 56 | 57 | if ($store->get($urlBase . "bar")) { 58 | throw new Exception("/bar should not be indexed, as it should not be trusted"); 59 | } 60 | 61 | if ($store->get($url . "-foo")) { 62 | throw new Exception("/test-schema-foo should not be indexed, as it should not be trusted"); 63 | } 64 | 65 | if ($store->get("http://somewhere-else.com/test-schema")) { 66 | throw new Exception("http://somewhere-else.com/test-schema should not be indexed, as it should not be trusted"); 67 | } 68 | 69 | $store->add($url, $schema, TRUE); 70 | 71 | if (!recursiveEqual($store->get($urlBase . "bar"), $schema->properties->bar)) { 72 | throw new Exception("/bar not found"); 73 | } -------------------------------------------------------------------------------- /test-utils.php: -------------------------------------------------------------------------------- 1 | $value) { 10 | if (!isset($b->$key)) { 11 | return FALSE; 12 | } 13 | if (!recursiveEqual($value, $b->$key)) { 14 | return FALSE; 15 | } 16 | } 17 | foreach ($b as $key => $value) { 18 | if (!isset($a->$key)) { 19 | return FALSE; 20 | } 21 | } 22 | return TRUE; 23 | } 24 | if (is_array($a)) { 25 | if (!is_array($b)) { 26 | return FALSE; 27 | } 28 | foreach ($a as $key => $value) { 29 | if (!isset($b[$key])) { 30 | return FALSE; 31 | } 32 | if (!recursiveEqual($value, $b[$key])) { 33 | return FALSE; 34 | } 35 | } 36 | foreach ($b as $key => $value) { 37 | if (!isset($a[$key])) { 38 | return FALSE; 39 | } 40 | } 41 | return TRUE; 42 | } 43 | return $a === $b; 44 | } 45 | 46 | 47 | function pointerGet(&$value, $path = "", $strict = FALSE) 48 | { 49 | if ($path == "") { 50 | return $value; 51 | } else if ($path[0] != "/") { 52 | throw new Exception("Invalid path: $path"); 53 | } 54 | $parts = explode("/", $path); 55 | array_shift($parts); 56 | foreach ($parts as $part) { 57 | $part = str_replace("~1", "/", $part); 58 | $part = str_replace("~0", "~", $part); 59 | if (is_array($value) && is_numeric($part)) { 60 | $value = & $value[$part]; 61 | } else if (is_object($value)) { 62 | if (isset($value->$part)) { 63 | $value = & $value->$part; 64 | } else if ($strict) { 65 | throw new Exception("Path does not exist: $path"); 66 | } else { 67 | return NULL; 68 | } 69 | } else if ($strict) { 70 | throw new Exception("Path does not exist: $path"); 71 | } else { 72 | return NULL; 73 | } 74 | } 75 | return $value; 76 | } 77 | 78 | 79 | function pointerJoin($parts) 80 | { 81 | $result = ""; 82 | foreach ($parts as $part) { 83 | $part = str_replace("~", "~0", $part); 84 | $part = str_replace("/", "~1", $part); 85 | $result .= "/" . $part; 86 | } 87 | return $result; 88 | } 89 | 90 | -------------------------------------------------------------------------------- /tests/01 - pure validation/01 - basic constraints/01 - enum.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "Enum passes (string comparison)", 5 | "schema": { 6 | "enum": ["one", "two", "three"] 7 | }, 8 | "data": "two", 9 | "result": { 10 | "/valid": true 11 | } 12 | }, 13 | { 14 | "method": "validate", 15 | "title": "Enum passes (object comparison)", 16 | "schema": { 17 | "enum": [{"one": 1}, {"two": 2}, {"three": 3}] 18 | }, 19 | "data": {"two": 2}, 20 | "result": { 21 | "/valid": true 22 | } 23 | }, 24 | { 25 | "method": "validate", 26 | "title": "Enum fails (string comparison)", 27 | "schema": { 28 | "enum": [1, 2, 3] 29 | }, 30 | "data": 4, 31 | "result": { 32 | "/valid": false, 33 | "/errors/0/code": 1, 34 | "/errors/0/dataPath": "", 35 | "/errors/0/schemaPath": "/enum", 36 | "/errors/0/message": "Value must be one of the enum options" 37 | } 38 | }, 39 | { 40 | "method": "validate", 41 | "title": "Enum fails (ensure correct type)", 42 | "schema": { 43 | "enum": [1, 2, 3] 44 | }, 45 | "data": "2", 46 | "result": { 47 | "/valid": false, 48 | "/errors/0/code": 1, 49 | "/errors/0/dataPath": "", 50 | "/errors/0/schemaPath": "/enum", 51 | "/errors/0/message": "Value must be one of the enum options" 52 | } 53 | }, 54 | { 55 | "method": "validate", 56 | "title": "Enum fails (object comparison)", 57 | "schema": { 58 | "enum": [{"one": 1}, {"two": 2}, {"three": 3}] 59 | }, 60 | "data": {"two": 2, "three": 3}, 61 | "result": { 62 | "/valid": false, 63 | "/errors/0/code": 1, 64 | "/errors/0/dataPath": "", 65 | "/errors/0/schemaPath": "/enum", 66 | "/errors/0/message": "Value must be one of the enum options" 67 | } 68 | }, 69 | { 70 | "method": "validate", 71 | "title": "GitHub Issue #8", 72 | "schema": { 73 | "enum": ["one", "two", "three"] 74 | }, 75 | "data": 2, 76 | "result": { 77 | "/valid": false, 78 | "/errors/0/code": 1, 79 | "/errors/0/dataPath": "", 80 | "/errors/0/schemaPath": "/enum", 81 | "/errors/0/message": "Value must be one of the enum options" 82 | } 83 | } 84 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/01 - basic constraints/types/05 - number.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "Fails object", 5 | "schema": {"type": "number"}, 6 | "data": { 7 | "foo": "bar" 8 | }, 9 | "result": { 10 | "/valid": false, 11 | "/errors/0/code": 0, 12 | "/errors/0/dataPath": "", 13 | "/errors/0/schemaPath": "/type", 14 | "/errors/0/message": "Invalid type: object" 15 | } 16 | }, 17 | { 18 | "method": "validate", 19 | "title": "Passes array", 20 | "schema": {"type": "number"}, 21 | "data": [null], 22 | "result": { 23 | "/valid": false, 24 | "/errors/0/code": 0, 25 | "/errors/0/dataPath": "", 26 | "/errors/0/schemaPath": "/type", 27 | "/errors/0/message": "Invalid type: array" 28 | } 29 | }, 30 | { 31 | "method": "validate", 32 | "title": "Fails string", 33 | "schema": {"type": "number"}, 34 | "data": "test", 35 | "result": { 36 | "/valid": false, 37 | "/errors/0/code": 0, 38 | "/errors/0/dataPath": "", 39 | "/errors/0/schemaPath": "/type", 40 | "/errors/0/message": "Invalid type: string" 41 | } 42 | }, 43 | { 44 | "method": "validate", 45 | "title": "Passes non-integer number", 46 | "schema": {"type": "number"}, 47 | "data": -0.001, 48 | "result": { 49 | "/valid": true 50 | } 51 | }, 52 | { 53 | "method": "validate", 54 | "title": "Passes integer number", 55 | "schema": {"type": "number"}, 56 | "data": 5, 57 | "result": { 58 | "/valid": true 59 | } 60 | }, 61 | { 62 | "method": "validate", 63 | "title": "Fails boolean", 64 | "schema": {"type": "number"}, 65 | "data": false, 66 | "result": { 67 | "/valid": false, 68 | "/errors/0/code": 0, 69 | "/errors/0/dataPath": "", 70 | "/errors/0/schemaPath": "/type", 71 | "/errors/0/message": "Invalid type: boolean" 72 | } 73 | }, 74 | { 75 | "method": "validate", 76 | "title": "Fails null", 77 | "schema": {"type": "number"}, 78 | "data": null, 79 | "result": { 80 | "/valid": false, 81 | "/errors/0/code": 0, 82 | "/errors/0/dataPath": "", 83 | "/errors/0/schemaPath": "/type", 84 | "/errors/0/message": "Invalid type: null" 85 | } 86 | } 87 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/02 - object constraints/06 - min-max properties.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "minProperties passes", 5 | "schema": { 6 | "minProperties": 1 7 | }, 8 | "data": { 9 | "foo": true 10 | }, 11 | "result": { 12 | "/valid": true 13 | } 14 | }, 15 | { 16 | "method": "validate", 17 | "title": "minProperties fails", 18 | "schema": { 19 | "minProperties": 1 20 | }, 21 | "data": {}, 22 | "result": { 23 | "/valid": false, 24 | "/errors/0/code": 300, 25 | "/errors/0/dataPath": "", 26 | "/errors/0/schemaPath": "/minProperties", 27 | "/errors/0/message": "Object cannot be empty" 28 | } 29 | }, 30 | { 31 | "method": "validate", 32 | "title": "minProperties fails plural", 33 | "schema": { 34 | "minProperties": 2 35 | }, 36 | "data": {}, 37 | "result": { 38 | "/valid": false, 39 | "/errors/0/code": 300, 40 | "/errors/0/dataPath": "", 41 | "/errors/0/schemaPath": "/minProperties", 42 | "/errors/0/message": "Object must have at least 2 defined properties" 43 | } 44 | }, 45 | { 46 | "method": "validate", 47 | "title": "maxProperties passes", 48 | "schema": { 49 | "maxProperties": 1 50 | }, 51 | "data": { 52 | "foo": true 53 | }, 54 | "result": { 55 | "/valid": true 56 | } 57 | }, 58 | { 59 | "method": "validate", 60 | "title": "maxProperties fails", 61 | "schema": { 62 | "maxProperties": 1 63 | }, 64 | "data": { 65 | "foo": true, 66 | "bar": true 67 | }, 68 | "result": { 69 | "/valid": false, 70 | "/errors/0/code": 301, 71 | "/errors/0/dataPath": "", 72 | "/errors/0/schemaPath": "/minProperties", 73 | "/errors/0/message": "Object must have at most one defined property" 74 | } 75 | }, 76 | { 77 | "method": "validate", 78 | "title": "maxProperties fails plural", 79 | "schema": { 80 | "maxProperties": 2 81 | }, 82 | "data": { 83 | "foo": true, 84 | "bar": true, 85 | "baz": true 86 | }, 87 | "result": { 88 | "/valid": false, 89 | "/errors/0/code": 301, 90 | "/errors/0/dataPath": "", 91 | "/errors/0/schemaPath": "/minProperties", 92 | "/errors/0/message": "Object must have at most 2 defined properties" 93 | } 94 | } 95 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/01 - basic constraints/types/06- integer.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "Fails object", 5 | "schema": {"type": "integer"}, 6 | "data": { 7 | "foo": "bar" 8 | }, 9 | "result": { 10 | "/valid": false, 11 | "/errors/0/code": 0, 12 | "/errors/0/dataPath": "", 13 | "/errors/0/schemaPath": "/type", 14 | "/errors/0/message": "Invalid type: object" 15 | } 16 | }, 17 | { 18 | "method": "validate", 19 | "title": "Passes array", 20 | "schema": {"type": "integer"}, 21 | "data": [null], 22 | "result": { 23 | "/valid": false, 24 | "/errors/0/code": 0, 25 | "/errors/0/dataPath": "", 26 | "/errors/0/schemaPath": "/type", 27 | "/errors/0/message": "Invalid type: array" 28 | } 29 | }, 30 | { 31 | "method": "validate", 32 | "title": "Fails string", 33 | "schema": {"type": "integer"}, 34 | "data": "test", 35 | "result": { 36 | "/valid": false, 37 | "/errors/0/code": 0, 38 | "/errors/0/dataPath": "", 39 | "/errors/0/schemaPath": "/type", 40 | "/errors/0/message": "Invalid type: string" 41 | } 42 | }, 43 | { 44 | "method": "validate", 45 | "title": "Fails non-integer number", 46 | "schema": {"type": "integer"}, 47 | "data": -5.2, 48 | "result": { 49 | "/valid": false, 50 | "/errors/0/code": 0, 51 | "/errors/0/dataPath": "", 52 | "/errors/0/schemaPath": "/type", 53 | "/errors/0/message": "Invalid type: number" 54 | } 55 | }, 56 | { 57 | "method": "validate", 58 | "title": "Passes integer number", 59 | "schema": {"type": "integer"}, 60 | "data": 5, 61 | "result": { 62 | "/valid": true 63 | } 64 | }, 65 | { 66 | "method": "validate", 67 | "title": "Fails boolean", 68 | "schema": {"type": "integer"}, 69 | "data": false, 70 | "result": { 71 | "/valid": false, 72 | "/errors/0/code": 0, 73 | "/errors/0/dataPath": "", 74 | "/errors/0/schemaPath": "/type", 75 | "/errors/0/message": "Invalid type: boolean" 76 | } 77 | }, 78 | { 79 | "method": "validate", 80 | "title": "Fails null", 81 | "schema": {"type": "integer"}, 82 | "data": null, 83 | "result": { 84 | "/valid": false, 85 | "/errors/0/code": 0, 86 | "/errors/0/dataPath": "", 87 | "/errors/0/schemaPath": "/type", 88 | "/errors/0/message": "Invalid type: null" 89 | } 90 | } 91 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/03 - array constraints/02 - additionalItems.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "additionalItems type-check passes for single-length", 5 | "schema": { 6 | "items": [ 7 | {"type": "boolean"} 8 | ], 9 | "additionalItems": {"type": "integer"} 10 | }, 11 | "data": [true], 12 | "result": { 13 | "/valid": true 14 | } 15 | }, 16 | { 17 | "method": "validate", 18 | "title": "additionalItems type-check passes for surplus length", 19 | "schema": { 20 | "items": [ 21 | {"type": "boolean"} 22 | ], 23 | "additionalItems": {"type": "integer"} 24 | }, 25 | "data": [true, 0, 1, 2], 26 | "result": { 27 | "/valid": true 28 | } 29 | }, 30 | { 31 | "method": "validate", 32 | "title": "additionalItems type-check fails for surplus length", 33 | "schema": { 34 | "items": [ 35 | {"type": "boolean"} 36 | ], 37 | "additionalItems": {"type": "integer"} 38 | }, 39 | "data": [true, 0, 1, "two"], 40 | "result": { 41 | "/valid": false, 42 | "/errors/0/code": 0, 43 | "/errors/0/dataPath": "/3", 44 | "/errors/0/schemaPath": "/additionalItems/type" 45 | } 46 | }, 47 | { 48 | "method": "validate", 49 | "title": "additionalItems:true passes for surplus length", 50 | "schema": { 51 | "items": [ 52 | {"type": "boolean"} 53 | ], 54 | "additionalItems": true 55 | }, 56 | "data": [true, 0, 1, 2], 57 | "result": { 58 | "/valid": true 59 | } 60 | }, 61 | { 62 | "method": "validate", 63 | "title": "additionalItems:false fails for surplus length", 64 | "schema": { 65 | "items": [ 66 | {"type": "boolean"} 67 | ], 68 | "additionalItems": false 69 | }, 70 | "data": [true, 0], 71 | "result": { 72 | "/valid": false, 73 | "/errors/0/code": 403, 74 | "/errors/0/dataPath": "/1", 75 | "/errors/0/schemaPath": "/additionalItems", 76 | "/errors/0/message": "Additional items (index 1 or more) are not allowed" 77 | } 78 | }, 79 | { 80 | "method": "validate", 81 | "title": "additionalItems ignored for non-tuple-typing", 82 | "schema": { 83 | "items": {"type": "boolean"}, 84 | "additionalItems": {"type": "integer"} 85 | }, 86 | "data": [true], 87 | "result": { 88 | "/valid": true 89 | } 90 | }, 91 | { 92 | "method": "validate", 93 | "title": "additionalItems ignored when \"items\" not specified", 94 | "schema": { 95 | "additionalItems": {"type": "integer"} 96 | }, 97 | "data": [true], 98 | "result": { 99 | "/valid": true 100 | } 101 | } 102 | ] -------------------------------------------------------------------------------- /tests/02 - coercive validation/02 - missing properties.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "coerce", 4 | "title": "Insert missing properties from default", 5 | "schema": { 6 | "properties": { 7 | "foo": {}, 8 | "bar": { 9 | "default": "default value" 10 | } 11 | }, 12 | "required": ["foo", "bar"] 13 | }, 14 | "data": { 15 | "foo": true 16 | }, 17 | "result": { 18 | "/valid": true, 19 | "/value": { 20 | "foo": true, 21 | "bar": "default value" 22 | } 23 | } 24 | }, 25 | { 26 | "method": "coerce", 27 | "title": "Insert missing properties from available type (integer)", 28 | "schema": { 29 | "properties": { 30 | "foo": {}, 31 | "bar": { 32 | "type": "integer" 33 | } 34 | }, 35 | "required": ["foo", "bar"] 36 | }, 37 | "data": { 38 | "foo": true 39 | }, 40 | "result": { 41 | "/valid": true, 42 | "/value": { 43 | "foo": true, 44 | "bar": 0 45 | } 46 | } 47 | }, 48 | { 49 | "method": "coerce", 50 | "title": "Insert missing properties from available type (string)", 51 | "schema": { 52 | "properties": { 53 | "foo": {}, 54 | "bar": { 55 | "type": ["string"] 56 | } 57 | }, 58 | "required": ["foo", "bar"] 59 | }, 60 | "data": { 61 | "foo": true 62 | }, 63 | "result": { 64 | "/valid": true, 65 | "/value": { 66 | "foo": true, 67 | "bar": "" 68 | } 69 | } 70 | }, 71 | { 72 | "method": "coerce", 73 | "title": "Insert missing properties from available type (boolean preferred to string)", 74 | "schema": { 75 | "properties": { 76 | "foo": {}, 77 | "bar": { 78 | "type": ["string", "boolean"] 79 | } 80 | }, 81 | "required": ["foo", "bar"] 82 | }, 83 | "data": { 84 | "foo": true 85 | }, 86 | "result": { 87 | "/valid": true, 88 | "/value": { 89 | "foo": true, 90 | "bar": true 91 | } 92 | } 93 | }, 94 | { 95 | "method": "coerce", 96 | "title": "Insert missing properties - recurse into sub-objects", 97 | "schema": { 98 | "properties": { 99 | "foo": {}, 100 | "bar": { 101 | "type": "object", 102 | "properties": { 103 | "x": {"type": "number"}, 104 | "y": {"type": "number", "default": 16} 105 | }, 106 | "required": ["x", "y"] 107 | } 108 | }, 109 | "required": ["foo", "bar"] 110 | }, 111 | "data": { 112 | "foo": true 113 | }, 114 | "result": { 115 | "/valid": true, 116 | "/value": { 117 | "foo": true, 118 | "bar": { 119 | "x": 0, 120 | "y": 16 121 | } 122 | } 123 | } 124 | } 125 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/02 - object constraints/03 - additionalProperties.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "additionalProperties type-check passes", 5 | "schema": { 6 | "additionalProperties": { 7 | "type": "boolean" 8 | } 9 | }, 10 | "data": { 11 | "foo": true 12 | }, 13 | "result": { 14 | "/valid": true 15 | } 16 | }, 17 | { 18 | "method": "validate", 19 | "title": "additionalProperties type-check fails", 20 | "schema": { 21 | "additionalProperties": { 22 | "type": "boolean" 23 | } 24 | }, 25 | "data": { 26 | "foo": "bar" 27 | }, 28 | "result": { 29 | "/valid": false, 30 | "/errors/0/code": 0, 31 | "/errors/0/dataPath": "/foo", 32 | "/errors/0/schemaPath": "/additionalProperties/type" 33 | } 34 | }, 35 | { 36 | "method": "validate", 37 | "title": "additionalProperties type-check passes non-objects", 38 | "schema": { 39 | "additionalProperties": { 40 | "type": "boolean" 41 | } 42 | }, 43 | "data": [null], 44 | "result": { 45 | "/valid": true 46 | } 47 | }, 48 | { 49 | "method": "validate", 50 | "title": "additionalProperties: false passes on known properties", 51 | "schema": { 52 | "properties": { 53 | "foo": {} 54 | }, 55 | "additionalProperties": false 56 | }, 57 | "data": { 58 | "foo": "bar" 59 | }, 60 | "result": { 61 | "/valid": true 62 | } 63 | }, 64 | { 65 | "method": "validate", 66 | "title": "additionalProperties: false fails on unknown properties", 67 | "schema": { 68 | "properties": { 69 | "foo": {} 70 | }, 71 | "additionalProperties": false 72 | }, 73 | "data": { 74 | "bar": "baz" 75 | }, 76 | "result": { 77 | "/valid": false, 78 | "/errors/0/code": 303, 79 | "/errors/0/dataPath": "/bar", 80 | "/errors/0/schemaPath": "/additionalProperties" 81 | } 82 | }, 83 | { 84 | "method": "validate", 85 | "title": "additionalProperties: false passes for non-objects", 86 | "schema": { 87 | "properties": { 88 | "foo": {} 89 | }, 90 | "additionalProperties": false 91 | }, 92 | "data": 68, 93 | "result": { 94 | "/valid": true 95 | } 96 | }, 97 | { 98 | "method": "validate", 99 | "title": "Github Issue #14", 100 | "schema": { 101 | "$schema": "http://json-schema.org/draft-04/schema#", 102 | "type": "object", 103 | "properties": { 104 | "id": { 105 | "type": "string" 106 | }, 107 | "name": { 108 | "type": "string" 109 | } 110 | }, 111 | "required": ["id", "name"], 112 | "additionalProperties": false 113 | }, 114 | "data": { 115 | "id": "12bfa5db-75e8-47cb-84cd-06520a867c45", 116 | "name": "Mike", 117 | "age": 71 118 | }, 119 | "result": { 120 | "/valid": false 121 | } 122 | } 123 | ] -------------------------------------------------------------------------------- /tests/03 - schema store/03 - references.php: -------------------------------------------------------------------------------- 1 | add($url, $schema); 25 | $schema = $store->get($url); 26 | if ($schema->properties->foo != $schema->definitions->foo) { 27 | throw new Exception('$ref was not resolved'); 28 | } 29 | 30 | // Add external $ref, and don't resolve it 31 | // While we're at it, use an array, not an object 32 | $schema = array( 33 | "title" => "Test schema 2", 34 | "properties" => array( 35 | "foo" => array('$ref' => "somewhere-else") 36 | ) 37 | ); 38 | $store->add($urlBase . "test-schema-2", $schema); 39 | $schema = $store->get($urlBase . "test-schema-2"); 40 | if (!$schema->properties->foo->{'$ref'}) { 41 | throw new Exception('$ref should still exist'); 42 | } 43 | if (!recursiveEqual($store->missing(), array($urlBase . "somewhere-else"))) { 44 | throw new Exception('$store->missing() is not correct: ' . json_encode($store->missing()) . ' is not ' . json_encode(array($urlBase . "somewhere-else"))); 45 | } 46 | 47 | $otherSchema = json_decode('{ 48 | "title": "Somewhere else", 49 | "items": [ 50 | {"$ref": "' . $urlBase . "test-schema-2" . '"} 51 | ] 52 | }'); 53 | $store->add($urlBase . "somewhere-else", $otherSchema); 54 | $fooSchema = $schema->properties->foo; 55 | if (property_exists($fooSchema, '$ref')) { 56 | throw new Exception('$ref should have been resolved'); 57 | } 58 | if ($fooSchema->title != "Somewhere else") { 59 | throw new Exception('$ref does not point to correct place'); 60 | } 61 | if ($fooSchema->items[0]->title != "Test schema 2") { 62 | throw new Exception('$ref in somewhere-else was not resolved'); 63 | } 64 | if (count($store->missing())) { 65 | throw new Exception('There should be no more missing schemas'); 66 | } 67 | 68 | // Add external $ref twice 69 | $schema = json_decode('{ 70 | "title": "Test schema 3 a", 71 | "properties": { 72 | "foo1": {"$ref": "' . $urlBase . 'test-schema-3-b#/foo"}, 73 | "foo2": {"$ref": "' . $urlBase . 'test-schema-3-b#/foo"} 74 | } 75 | }'); 76 | $store->add($urlBase . "test-schema-3-a", $schema); 77 | $schema = json_decode('{ 78 | "title": "Test schema 3 b", 79 | "foo": {"type": "object"} 80 | }'); 81 | $schema = $store->add($urlBase . "test-schema-3-b", $schema); 82 | $schema = $store->get($urlBase . "test-schema-3-a"); 83 | if (property_exists($schema->properties->foo1, '$ref')) { 84 | throw new Exception('$ref was not resolved for foo1'); 85 | } 86 | if (property_exists($schema->properties->foo2, '$ref')) { 87 | throw new Exception('$ref was not resolved for foo2'); 88 | } -------------------------------------------------------------------------------- /tests/02 - coercive validation/01 - simple type juggling.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "coerce", 4 | "title": "Numeric string -> number", 5 | "schema": {"type": "number"}, 6 | "data": "15.2", 7 | "result": { 8 | "/valid": true, 9 | "/value": 15.2 10 | } 11 | }, 12 | { 13 | "method": "coerce", 14 | "title": "Integer string -> integer", 15 | "schema": {"type": "integer"}, 16 | "data": "15", 17 | "result": { 18 | "/valid": true, 19 | "/value": 15 20 | } 21 | }, 22 | { 23 | "method": "coerce", 24 | "title": "Non-numerical string -!-> number", 25 | "schema": {"type": "number"}, 26 | "data": "test", 27 | "result": { 28 | "/valid": false 29 | } 30 | }, 31 | { 32 | "method": "coerce", 33 | "title": "Non-integer string -!-> integer", 34 | "schema": {"type": "integer"}, 35 | "data": "15.2", 36 | "result": { 37 | "/valid": false 38 | } 39 | }, 40 | { 41 | "method": "coerce", 42 | "title": "Number -> string", 43 | "schema": {"type": "string"}, 44 | "data": 68.2521, 45 | "result": { 46 | "/valid": true, 47 | "/value": "68.2521" 48 | } 49 | }, 50 | { 51 | "method": "coerce", 52 | "title": "Boolean -> string", 53 | "schema": {"type": "string"}, 54 | "data": false, 55 | "result": { 56 | "/valid": true, 57 | "/value": "false" 58 | } 59 | }, 60 | { 61 | "method": "coerce", 62 | "title": "Null -> string", 63 | "schema": {"type": "string"}, 64 | "data": null, 65 | "result": { 66 | "/valid": true, 67 | "/value": "" 68 | } 69 | }, 70 | { 71 | "method": "coerce", 72 | "title": "Boolean -> number (false)", 73 | "schema": {"type": "number"}, 74 | "data": false, 75 | "result": { 76 | "/valid": true, 77 | "/value": 0 78 | } 79 | }, 80 | { 81 | "method": "coerce", 82 | "title": "Boolean -> number (true)", 83 | "schema": {"type": "number"}, 84 | "data": true, 85 | "result": { 86 | "/valid": true, 87 | "/value": 1 88 | } 89 | }, 90 | { 91 | "method": "coerce", 92 | "title": "Boolean -> integer (false)", 93 | "schema": {"type": "integer"}, 94 | "data": false, 95 | "result": { 96 | "/valid": true, 97 | "/value": 0 98 | } 99 | }, 100 | { 101 | "method": "coerce", 102 | "title": "Boolean -> integer (true)", 103 | "schema": {"type": "integer"}, 104 | "data": true, 105 | "result": { 106 | "/valid": true, 107 | "/value": 1 108 | } 109 | }, 110 | { 111 | "method": "coerce", 112 | "title": "Number -> boolean", 113 | "schema": { 114 | "items": {"type": "boolean"} 115 | }, 116 | "data": [1, "1", "true", "yes", 2], 117 | "result": { 118 | "/valid": true, 119 | "/value": [true, true, true, true, true] 120 | } 121 | }, 122 | { 123 | "method": "coerce", 124 | "title": "Coerce to boolean (false)", 125 | "schema": { 126 | "items": {"type": "boolean"} 127 | }, 128 | "data": [0, "0", "false", "no", null], 129 | "result": { 130 | "/valid": true, 131 | "/value": [false, false, false, false, false] 132 | } 133 | } 134 | ] -------------------------------------------------------------------------------- /tests/01 - pure validation/02 - object constraints/05 - dependencies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "method": "validate", 4 | "title": "dependency required-check passes when property missing", 5 | "schema": { 6 | "dependencies": { 7 | "foo": { 8 | "required": ["bar"] 9 | } 10 | } 11 | }, 12 | "data": { 13 | "someOtherKey": true 14 | }, 15 | "result": { 16 | "/valid": true 17 | } 18 | }, 19 | { 20 | "method": "validate", 21 | "title": "dependency required-check fails when one property present", 22 | "schema": { 23 | "dependencies": { 24 | "foo": { 25 | "required": ["bar"] 26 | } 27 | } 28 | }, 29 | "data": { 30 | "foo": true 31 | }, 32 | "result": { 33 | "/valid": false, 34 | "/errors/0/code": 302, 35 | "/errors/0/dataPath": "", 36 | "/errors/0/schemaPath": "/dependencies/foo/required/0", 37 | "/errors/0/message": "Missing required property: bar" 38 | } 39 | }, 40 | { 41 | "method": "validate", 42 | "title": "dependency required-check passes when both properties exist", 43 | "schema": { 44 | "dependencies": { 45 | "foo": { 46 | "required": ["bar"] 47 | } 48 | } 49 | }, 50 | "data": { 51 | "foo": true, 52 | "bar": true 53 | }, 54 | "result": { 55 | "/valid": true 56 | } 57 | }, 58 | { 59 | "method": "validate", 60 | "title": "dependency string passes when property missing", 61 | "schema": { 62 | "dependencies": { 63 | "foo": "bar" 64 | } 65 | }, 66 | "data": { 67 | "someOtherKey": true 68 | }, 69 | "result": { 70 | "/valid": true 71 | } 72 | }, 73 | { 74 | "method": "validate", 75 | "title": "dependency string fails when one property present", 76 | "schema": { 77 | "dependencies": { 78 | "foo": "bar" 79 | } 80 | }, 81 | "data": { 82 | "foo": true 83 | }, 84 | "result": { 85 | "/valid": false, 86 | "/errors/0/code": 304, 87 | "/errors/0/dataPath": "", 88 | "/errors/0/schemaPath": "/dependencies/foo", 89 | "/errors/0/message": "Property foo depends on bar" 90 | } 91 | }, 92 | { 93 | "method": "validate", 94 | "title": "dependency string passes when both properties exist", 95 | "schema": { 96 | "dependencies": { 97 | "foo": "bar" 98 | } 99 | }, 100 | "data": { 101 | "foo": true, 102 | "bar": true 103 | }, 104 | "result": { 105 | "/valid": true 106 | } 107 | }, 108 | { 109 | "method": "validate", 110 | "title": "dependency array passes when property missing", 111 | "schema": { 112 | "dependencies": { 113 | "foo": ["bar", "baz"] 114 | } 115 | }, 116 | "data": { 117 | "someOtherKey": true 118 | }, 119 | "result": { 120 | "/valid": true 121 | } 122 | }, 123 | { 124 | "method": "validate", 125 | "title": "dependency array fails when one property missing", 126 | "schema": { 127 | "dependencies": { 128 | "foo": ["bar", "baz"] 129 | } 130 | }, 131 | "data": { 132 | "foo": true, 133 | "bar": true 134 | }, 135 | "result": { 136 | "/valid": false, 137 | "/errors/0/code": 304, 138 | "/errors/0/dataPath": "", 139 | "/errors/0/schemaPath": "/dependencies/foo/1", 140 | "/errors/0/message": "Property foo depends on baz" 141 | } 142 | }, 143 | { 144 | "method": "validate", 145 | "title": "dependency array passes when both properties exist", 146 | "schema": { 147 | "dependencies": { 148 | "foo": ["bar", "baz"] 149 | } 150 | }, 151 | "data": { 152 | "foo": true, 153 | "bar": true, 154 | "baz": true 155 | }, 156 | "result": { 157 | "/valid": true 158 | } 159 | } 160 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsv4-php 2 | 3 | ## A (coercive) JSON Schema v4 Validator for PHP 4 | 5 | `jsv4-php` is a data validator, using version 4 JSON Schemas. 6 | 7 | Just include `jsv4.php` from your code, and use the static methods on the `Jsv4` class it defines. 8 | 9 | Usage: 10 | 11 | ### `Jsv4::validate($data, $schema)` 12 | 13 | This usage returns an object of the following shape. 14 | ```json 15 | { 16 | "valid": true/false, 17 | "errors": [ 18 | {...} 19 | ] 20 | } 21 | ``` 22 | 23 | The values in the `errors` array are similar to those for [tv4](https://github.com/geraintluff/tv4) (a similar project): 24 | 25 | ```json 26 | { 27 | "code": 0, 28 | "message": "Invalid type: string", 29 | "dataPath": "/intKey", 30 | "schemaKey": "/properties/intKey/type" 31 | } 32 | ``` 33 | 34 | The `code` property corresponds to a constant corresponding to the nature of the validation error, e.g. `JSV4_INVALID_TYPE`. The names of these constants (and their values) match up exactly with the [constants from tv4](https://github.com/geraintluff/tv4/blob/master/source/api.js). 35 | 36 | ### `Jsv4::isValid($data, $schema)` 37 | 38 | If you just want to know the validation status, and don't care what the errors actually are, then this is a more concise way of getting it. 39 | 40 | It returns a boolean indicating whether the data correctly followed the schema. 41 | 42 | ### `Jsv4::coerce($data, $schema)` 43 | 44 | Sometimes, the data is not quite the correct shape - but it could be *made* the correct shape by simple modifications. 45 | 46 | If you call `Jsv4::coerce($data, $schema)`, then it will attempt to change the data. 47 | 48 | If it is successful, then a modified version of the data can be found in `$result->value`. 49 | 50 | It's not psychic - in fact, it's quite limited. What it currently does is: 51 | 52 | ### Type-coercion for scalar types 53 | 54 | Perhaps you are using data from `$_GET`, so everything's a string, but the schema says certain values should be integers or booleans. 55 | 56 | `Jsv4::coerce()` will attempt to convert strings to numbers/booleans *only where the schema says*, leaving other numerically-value strings as strings. 57 | 58 | ### Missing properties 59 | 60 | Perhaps the API needs a complete object (described using `"required"` in the schema), but only a partial one was supplied. 61 | 62 | `Jsv4::coerce()` will attempt to insert appropriate values for the missing properties, using a default (if it is defined in a nearby `"properties"` entry) or by creating a value if it knows the type. 63 | 64 | ## The `SchemaStore` class 65 | 66 | This class represents a collection of schemas. You include it from `schema-store.php`, and use it like this: 67 | ```php 68 | $store = new SchemaStore(); 69 | $store->add($url, $schema); 70 | $retrieved = $store->get($url); 71 | ``` 72 | 73 | It can handle: 74 | 75 | * Fragments in URLs, using both JSON Pointer fragments and identification using `"id"` 76 | * Converts URIs in `"id"` and `"$ref"` to absolute (where possible) 77 | * Resolves `"$ref"`s, splicing the resulting value into the schema 78 | * Converts associative PHP arrays to objects - you can express your schema natively, but what you retrieve from the store is always an object. 79 | * Adds sub-schemas according to their `"id"` - by default, this only happens if `"id"` is a sub-path of the current schema URL. 80 | 81 | If the schemas being added are "trusted", then an extra argument can be supplied: `$store->add($url, $schema, TRUE)`. In that case, the value of `"id"` is *always* believed for all sub-schemas. 82 | 83 | A list of "missing" schemas (unresolved `"$ref"`s) can be retrieved used `$store->missing()`. 84 | 85 | This class does not depend on `jsv4.php` at all - it just deals with raw schema objects. As such, it could (hopefully) be used with other validators with minimal fuss. 86 | 87 | ## Tests 88 | 89 | The tests can be run using `test.php` (from the command-line or via the web). 90 | 91 | ## License 92 | 93 | This code is released under a do-anything-you-like "public domain" license (see `LICENSE.txt`). 94 | 95 | It is also released under an MIT-style license (see `LICENSE-MIT.txt`) because there is sometimes benefit in having a recognised open-source license. -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 | method == "validate") { 24 | $result = Validator::validate($test->data, $test->schema); 25 | } else if ($test->method == "isValid") { 26 | $result = Validator::isValid($test->data, $test->schema); 27 | } else if ($test->method == "coerce") { 28 | $result = Validator::coerce($test->data, $test->schema); 29 | } else { 30 | $failedTests[$key][] = ("Unknown method: {$test->method}"); 31 | return; 32 | } 33 | if (is_object($test->result)) { 34 | foreach ($test->result as $path => $expectedValue) { 35 | $actualValue = pointerGet($result, $path, TRUE); 36 | if (!recursiveEqual($actualValue, $expectedValue)) { 37 | $failedTests[$key][] = "$path does not match - should be:\n " . json_encode($expectedValue) . "\nwas:\n " . json_encode($actualValue); 38 | } 39 | } 40 | } else { 41 | if (!recursiveEqual($test->result, $result)) { 42 | $failedTests[$key][] = "$path does not match - should be:\n " . json_encode($test->result) . "\nwas:\n " . json_encode($result); 43 | } 44 | } 45 | } catch (\Exception $e) { 46 | $failedTests[$key][] = $e->getMessage(); 47 | $failedTests[$key][] .= " " . str_replace("\n", "\n ", $e->getTraceAsString()); 48 | } 49 | } 50 | 51 | 52 | function runPhpTest($key, $filename) 53 | { 54 | global $totalTestCount; 55 | global $failedTests; 56 | $totalTestCount++; 57 | 58 | try { 59 | include_once $filename; 60 | } catch (\Exception $e) { 61 | $failedTests[$key][] = $e->getMessage(); 62 | $failedTests[$key][] .= " " . str_replace("\n", "\n ", $e->getTraceAsString()); 63 | } 64 | } 65 | 66 | 67 | function runTests($directory, $indent = "") 68 | { 69 | global $failedTests; 70 | if ($directory[strlen($directory) - 1] != "/") { 71 | $directory .= "/"; 72 | } 73 | $baseName = basename($directory); 74 | 75 | $testCount = 0; 76 | $testFileCount = 0; 77 | 78 | $entries = scandir($directory); 79 | foreach ($entries as $entry) { 80 | $filename = $directory . $entry; 81 | if (stripos($entry, '.php') && is_file($filename)) { 82 | $key = substr($filename, 0, strlen($filename) - 4); 83 | runPhpTest($key, $filename); 84 | } else if (stripos($entry, '.json') && is_file($filename)) { 85 | $testFileCount++; 86 | $tests = json_decode(file_get_contents($filename)); 87 | if ($tests == NULL) { 88 | $testCount++; 89 | $failedTests[$filename] = "Error parsing JSON"; 90 | continue; 91 | } 92 | if (!is_array($tests)) { 93 | $tests = array($tests); 94 | } 95 | foreach ($tests as $index => $test) { 96 | $key = substr($filename, 0, strlen($filename) - 5); 97 | if (isset($test->title)) { 98 | $key .= ": {$test->title}"; 99 | } else { 100 | $key .= ": #{$index}"; 101 | } 102 | runJsonTest($key, $test); 103 | $testCount++; 104 | } 105 | } 106 | } 107 | if ($testCount) { 108 | echo "{$indent}{$baseName}/ \t({$testCount} tests in {$testFileCount} files)\n"; 109 | } else { 110 | echo "{$indent}{$baseName}/\n"; 111 | } 112 | foreach ($entries as $entry) { 113 | $filename = $directory . $entry; 114 | if (strpos($entry, '.') === FALSE && is_dir($filename)) { 115 | runTests($filename, $indent . str_repeat(" ", strlen($baseName) + 1)); 116 | } 117 | } 118 | } 119 | 120 | 121 | runTests("tests/"); 122 | 123 | echo "\n\n"; 124 | if (count($failedTests) == 0) { 125 | echo "Passed all {$totalTestCount} tests\n"; 126 | } else { 127 | echo "Failed " . count($failedTests) . "/{$totalTestCount} tests\n"; 128 | foreach ($failedTests as $key => $failedTest) { 129 | if (is_array($failedTest)) { 130 | $failedTest = implode("\n", $failedTest); 131 | } 132 | echo "\n"; 133 | echo "FAILED $key:\n"; 134 | echo str_repeat("-", strlen($key) + 10) . "\n"; 135 | echo " | " . str_replace("\n", "\n | ", $failedTest) . "\n"; 136 | } 137 | } -------------------------------------------------------------------------------- /src/Jsv4/SchemaStore.php: -------------------------------------------------------------------------------- 1 | $part)) { 25 | $value = & $value->$part; 26 | } else if ($strict) { 27 | throw new Exception("Path does not exist: $path"); 28 | } else { 29 | return NULL; 30 | } 31 | } else if ($strict) { 32 | throw new Exception("Path does not exist: $path"); 33 | } else { 34 | return NULL; 35 | } 36 | } 37 | return $value; 38 | } 39 | 40 | 41 | private static function isNumericArray($array) 42 | { 43 | $count = count($array); 44 | for ($i = 0; $i < $count; $i++) { 45 | if (!isset($array[$i])) { 46 | return FALSE; 47 | } 48 | } 49 | return TRUE; 50 | } 51 | 52 | 53 | private static function resolveUrl($base, $relative) 54 | { 55 | if (parse_url($relative, PHP_URL_SCHEME) != '') { 56 | // It's already absolute 57 | return $relative; 58 | } 59 | $baseParts = parse_url($base); 60 | if ($relative[0] == "?") { 61 | $baseParts['query'] = substr($relative, 1); 62 | unset($baseParts['fragment']); 63 | } else if ($relative[0] == "#") { 64 | $baseParts['fragment'] = substr($relative, 1); 65 | } else if ($relative[0] == "/") { 66 | if ($relative[1] == "/") { 67 | return $baseParts['scheme'] . $relative; 68 | } 69 | $baseParts['path'] = $relative; 70 | unset($baseParts['query']); 71 | unset($baseParts['fragment']); 72 | } else { 73 | $basePathParts = explode("/", $baseParts['path']); 74 | $relativePathParts = explode("/", $relative); 75 | array_pop($basePathParts); 76 | while (count($relativePathParts)) { 77 | if ($relativePathParts[0] == "..") { 78 | array_shift($relativePathParts); 79 | if (count($basePathParts)) { 80 | array_pop($basePathParts); 81 | } 82 | } else if ($relativePathParts[0] == ".") { 83 | array_shift($relativePathParts); 84 | } else { 85 | array_push($basePathParts, array_shift($relativePathParts)); 86 | } 87 | } 88 | $baseParts['path'] = implode("/", $basePathParts); 89 | if ($baseParts['path'][0] != '/') { 90 | $baseParts['path'] = "/" . $baseParts['path']; 91 | } 92 | } 93 | 94 | $result = ""; 95 | if (isset($baseParts['scheme'])) { 96 | $result .= $baseParts['scheme'] . "://"; 97 | if (isset($baseParts['user'])) { 98 | $result .= ":" . $baseParts['user']; 99 | if (isset($baseParts['pass'])) { 100 | $result .= ":" . $baseParts['pass']; 101 | } 102 | $result .= "@"; 103 | } 104 | $result .= $baseParts['host']; 105 | if (isset($baseParts['port'])) { 106 | $result .= ":" . $baseParts['port']; 107 | } 108 | } 109 | $result .= $baseParts["path"]; 110 | if (isset($baseParts['query'])) { 111 | $result .= "?" . $baseParts['query']; 112 | } 113 | if (isset($baseParts['fragment'])) { 114 | $result .= "#" . $baseParts['fragment']; 115 | } 116 | return $result; 117 | } 118 | 119 | 120 | private $schemas = array(); 121 | private $refs = array(); 122 | 123 | public function missing() 124 | { 125 | return array_keys($this->refs); 126 | } 127 | 128 | 129 | public function add($url, $schema, $trusted = FALSE) 130 | { 131 | $urlParts = explode("#", $url); 132 | $baseUrl = array_shift($urlParts); 133 | $fragment = urldecode(implode("#", $urlParts)); 134 | 135 | $trustBase = explode("?", $baseUrl); 136 | $trustBase = $trustBase[0]; 137 | 138 | $this->schemas[$url] = & $schema; 139 | $this->normalizeSchema($url, $schema, $trusted ? TRUE : $trustBase); 140 | if ($fragment == "") { 141 | $this->schemas[$baseUrl] = $schema; 142 | } 143 | if (isset($this->refs[$baseUrl])) { 144 | foreach ($this->refs[$baseUrl] as $fullUrl => $refSchemas) { 145 | foreach ($refSchemas as &$refSchema) { 146 | $refSchema = $this->get($fullUrl); 147 | } 148 | unset($this->refs[$baseUrl][$fullUrl]); 149 | } 150 | if (isset($this->refs[$baseUrl]) && count($this->refs[$baseUrl]) === 0) { 151 | unset($this->refs[$baseUrl]); 152 | } 153 | } 154 | } 155 | 156 | 157 | private function normalizeSchema($url, &$schema, $trustPrefix = '') 158 | { 159 | if (is_array($schema) && !self::isNumericArray($schema)) { 160 | $schema = (object) $schema; 161 | } 162 | if (is_object($schema)) { 163 | if (isset($schema->{'$ref'})) { 164 | $refUrl = $schema->{'$ref'} = self::resolveUrl($url, $schema->{'$ref'}); 165 | if ($refSchema = $this->get($refUrl)) { 166 | $schema = $refSchema; 167 | return; 168 | } else { 169 | $urlParts = explode("#", $refUrl); 170 | $baseUrl = array_shift($urlParts); 171 | $fragment = urldecode(implode("#", $urlParts)); 172 | $this->refs[$baseUrl][$refUrl][] = & $schema; 173 | } 174 | } else if (isset($schema->id) && is_string($schema->id)) { 175 | $schema->id = $url = self::resolveUrl($url, $schema->id); 176 | $regex = '/^' . preg_quote($trustPrefix, '/') . '(?:[#\/?].*)?$/'; 177 | if (($trustPrefix === TRUE || preg_match($regex, $schema->id)) && !isset($this->schemas[$schema->id])) { 178 | $this->add($schema->id, $schema); 179 | } 180 | } 181 | foreach ($schema as $key => &$value) { 182 | if ($key != "enum") { 183 | self::normalizeSchema($url, $value, $trustPrefix); 184 | } 185 | } 186 | } else if (is_array($schema)) { 187 | foreach ($schema as &$value) { 188 | self::normalizeSchema($url, $value, $trustPrefix); 189 | } 190 | } 191 | } 192 | 193 | 194 | public function get($url) 195 | { 196 | if (isset($this->schemas[$url])) { 197 | return $this->schemas[$url]; 198 | } 199 | $urlParts = explode("#", $url); 200 | $baseUrl = array_shift($urlParts); 201 | $fragment = urldecode(implode("#", $urlParts)); 202 | if (isset($this->schemas[$baseUrl])) { 203 | $schema = $this->schemas[$baseUrl]; 204 | if ($schema && $fragment == "" || $fragment[0] == "/") { 205 | $schema = self::pointerGet($schema, $fragment); 206 | $this->add($url, $schema); 207 | return $schema; 208 | } 209 | } 210 | } 211 | 212 | 213 | } 214 | 215 | ?> 216 | -------------------------------------------------------------------------------- /src/Jsv4/Validator.php: -------------------------------------------------------------------------------- 1 | data = & $data; 47 | $this->schema = & $schema; 48 | $this->firstErrorOnly = $firstErrorOnly; 49 | $this->coerce = $coerce; 50 | $this->valid = TRUE; 51 | $this->errors = []; 52 | 53 | try { 54 | $this->checkTypes(); 55 | $this->checkEnum(); 56 | $this->checkObject(); 57 | $this->checkArray(); 58 | $this->checkString(); 59 | $this->checkNumber(); 60 | $this->checkComposite(); 61 | } catch (ValidationException $e) { 62 | 63 | } 64 | } 65 | 66 | 67 | static public function validate($data, $schema) 68 | { 69 | return new Validator($data, $schema); 70 | } 71 | 72 | 73 | static public function isValid($data, $schema) 74 | { 75 | $result = new Validator($data, $schema, TRUE); 76 | return $result->valid; 77 | } 78 | 79 | 80 | static public function coerce($data, $schema) 81 | { 82 | if (is_object($data) || is_array($data)) { 83 | $data = unserialize(serialize($data)); 84 | } 85 | $result = new Validator($data, $schema, FALSE, TRUE); 86 | if ($result->valid) { 87 | $result->value = $result->data; 88 | } 89 | return $result; 90 | } 91 | 92 | 93 | static public function pointerJoin($parts) 94 | { 95 | $result = ""; 96 | foreach ($parts as $part) { 97 | $part = str_replace("~", "~0", $part); 98 | $part = str_replace("/", "~1", $part); 99 | $result .= "/" . $part; 100 | } 101 | return $result; 102 | } 103 | 104 | 105 | static public function recursiveEqual($a, $b) 106 | { 107 | if (is_object($a)) { 108 | if (!is_object($b)) { 109 | return FALSE; 110 | } 111 | foreach ($a as $key => $value) { 112 | if (!isset($b->$key)) { 113 | return FALSE; 114 | } 115 | if (!self::recursiveEqual($value, $b->$key)) { 116 | return FALSE; 117 | } 118 | } 119 | foreach ($b as $key => $value) { 120 | if (!isset($a->$key)) { 121 | return FALSE; 122 | } 123 | } 124 | return TRUE; 125 | } 126 | if (is_array($a)) { 127 | if (!is_array($b)) { 128 | return FALSE; 129 | } 130 | foreach ($a as $key => $value) { 131 | if (!isset($b[$key])) { 132 | return FALSE; 133 | } 134 | if (!self::recursiveEqual($value, $b[$key])) { 135 | return FALSE; 136 | } 137 | } 138 | foreach ($b as $key => $value) { 139 | if (!isset($a[$key])) { 140 | return FALSE; 141 | } 142 | } 143 | return TRUE; 144 | } 145 | return $a === $b; 146 | } 147 | 148 | 149 | private function fail($code, $dataPath, $schemaPath, $errorMessage, $subErrors = NULL) 150 | { 151 | $this->valid = FALSE; 152 | $error = new ValidationException($code, $dataPath, $schemaPath, $errorMessage, $subErrors); 153 | $this->errors[] = $error; 154 | if ($this->firstErrorOnly) { 155 | throw $error; 156 | } 157 | } 158 | 159 | 160 | private function subResult(&$data, $schema, $allowCoercion = TRUE) 161 | { 162 | return new Validator($data, $schema, $this->firstErrorOnly, $allowCoercion && $this->coerce); 163 | } 164 | 165 | 166 | private function includeSubResult($subResult, $dataPrefix, $schemaPrefix) 167 | { 168 | if (!$subResult->valid) { 169 | $this->valid = FALSE; 170 | foreach ($subResult->errors as $error) { 171 | $this->errors[] = $error->prefix($dataPrefix, $schemaPrefix); 172 | } 173 | } 174 | } 175 | 176 | 177 | private function checkTypes() 178 | { 179 | if (isset($this->schema->type)) { 180 | $types = $this->schema->type; 181 | if (!is_array($types)) { 182 | $types = [$types]; 183 | } 184 | foreach ($types as $type) { 185 | if ($type == "object" && is_object($this->data)) { 186 | return; 187 | } elseif ($type == "array" && is_array($this->data)) { 188 | return; 189 | } elseif ($type == "string" && is_string($this->data)) { 190 | return; 191 | } elseif ($type == "number" && !is_string($this->data) && is_numeric($this->data)) { 192 | return; 193 | } elseif ($type == "integer" && is_int($this->data)) { 194 | return; 195 | } elseif ($type == "boolean" && is_bool($this->data)) { 196 | return; 197 | } elseif ($type == "null" && $this->data === NULL) { 198 | return; 199 | } 200 | } 201 | 202 | if ($this->coerce) { 203 | foreach ($types as $type) { 204 | if ($type == "number") { 205 | if (is_numeric($this->data)) { 206 | $this->data = (float) $this->data; 207 | return; 208 | } else if (is_bool($this->data)) { 209 | $this->data = $this->data ? 1 : 0; 210 | return; 211 | } 212 | } else if ($type == "integer") { 213 | if ((int) $this->data == $this->data) { 214 | $this->data = (int) $this->data; 215 | return; 216 | } 217 | } else if ($type == "string") { 218 | if (is_numeric($this->data)) { 219 | $this->data = "" . $this->data; 220 | return; 221 | } else if (is_bool($this->data)) { 222 | $this->data = ($this->data) ? "true" : "false"; 223 | return; 224 | } else if (is_null($this->data)) { 225 | $this->data = ""; 226 | return; 227 | } 228 | } else if ($type == "boolean") { 229 | if (is_numeric($this->data)) { 230 | $this->data = ($this->data != "0"); 231 | return; 232 | } else if ($this->data == "yes" || $this->data == "true") { 233 | $this->data = TRUE; 234 | return; 235 | } else if ($this->data == "no" || $this->data == "false") { 236 | $this->data = FALSE; 237 | return; 238 | } else if ($this->data == NULL) { 239 | $this->data = FALSE; 240 | return; 241 | } 242 | } 243 | } 244 | } 245 | 246 | $type = gettype($this->data); 247 | if ($type == "double") { 248 | $type = ((int) $this->data == $this->data) ? "integer" : "number"; 249 | } else if ($type == "NULL") { 250 | $type = "null"; 251 | } 252 | $this->fail(self::INVALID_TYPE, "", "/type", "Invalid type: $type"); 253 | } 254 | } 255 | 256 | 257 | private function checkEnum() 258 | { 259 | if (isset($this->schema->enum)) { 260 | foreach ($this->schema->enum as $option) { 261 | if (self::recursiveEqual($this->data, $option)) { 262 | return; 263 | } 264 | } 265 | $this->fail(self::ENUM_MISMATCH, "", "/enum", "Value must be one of the enum options"); 266 | } 267 | } 268 | 269 | 270 | private function checkObject() 271 | { 272 | if (!is_object($this->data)) { 273 | return; 274 | } 275 | if (isset($this->schema->required)) { 276 | foreach ($this->schema->required as $index => $key) { 277 | if (!array_key_exists($key, (array) $this->data)) { 278 | if ($this->coerce && $this->createValueForProperty($key)) { 279 | continue; 280 | } 281 | $this->fail(self::OBJECT_REQUIRED, "", "/required/{$index}", "Missing required property: {$key}"); 282 | } 283 | } 284 | } 285 | $checkedProperties = []; 286 | if (isset($this->schema->properties)) { 287 | foreach ($this->schema->properties as $key => $subSchema) { 288 | $checkedProperties[$key] = TRUE; 289 | if (array_key_exists($key, (array) $this->data)) { 290 | $subResult = $this->subResult($this->data->$key, $subSchema); 291 | $this->includeSubResult($subResult, self::pointerJoin(array($key)), self::pointerJoin(array("properties", $key))); 292 | } 293 | } 294 | } 295 | if (isset($this->schema->patternProperties)) { 296 | foreach ($this->schema->patternProperties as $pattern => $subSchema) { 297 | foreach ($this->data as $key => &$subValue) { 298 | if (preg_match("/" . str_replace("/", "\\/", $pattern) . "/", $key)) { 299 | $checkedProperties[$key] = TRUE; 300 | $subResult = $this->subResult($this->data->$key, $subSchema); 301 | $this->includeSubResult($subResult, self::pointerJoin(array($key)), self::pointerJoin(array("patternProperties", $pattern))); 302 | } 303 | } 304 | } 305 | } 306 | if (isset($this->schema->additionalProperties)) { 307 | $additionalProperties = $this->schema->additionalProperties; 308 | foreach ($this->data as $key => &$subValue) { 309 | if (isset($checkedProperties[$key])) { 310 | continue; 311 | } 312 | if (!$additionalProperties) { 313 | $this->fail(self::OBJECT_ADDITIONAL_PROPERTIES, self::pointerJoin(array($key)), "/additionalProperties", "Additional properties not allowed"); 314 | } else if (is_object($additionalProperties)) { 315 | $subResult = $this->subResult($subValue, $additionalProperties); 316 | $this->includeSubResult($subResult, self::pointerJoin(array($key)), "/additionalProperties"); 317 | } 318 | } 319 | } 320 | if (isset($this->schema->dependencies)) { 321 | foreach ($this->schema->dependencies as $key => $dep) { 322 | if (!isset($this->data->$key)) { 323 | continue; 324 | } 325 | if (is_object($dep)) { 326 | $subResult = $this->subResult($this->data, $dep); 327 | $this->includeSubResult($subResult, "", self::pointerJoin(array("dependencies", $key))); 328 | } else if (is_array($dep)) { 329 | foreach ($dep as $index => $depKey) { 330 | if (!isset($this->data->$depKey)) { 331 | $this->fail(self::OBJECT_DEPENDENCY_KEY, "", self::pointerJoin(array("dependencies", $key, $index)), "Property $key depends on $depKey"); 332 | } 333 | } 334 | } else { 335 | if (!isset($this->data->$dep)) { 336 | $this->fail(self::OBJECT_DEPENDENCY_KEY, "", self::pointerJoin(array("dependencies", $key)), "Property $key depends on $dep"); 337 | } 338 | } 339 | } 340 | } 341 | if (isset($this->schema->minProperties)) { 342 | if (count(get_object_vars($this->data)) < $this->schema->minProperties) { 343 | $this->fail(self::OBJECT_PROPERTIES_MINIMUM, "", "/minProperties", ($this->schema->minProperties == 1) ? "Object cannot be empty" : "Object must have at least {$this->schema->minProperties} defined properties"); 344 | } 345 | } 346 | if (isset($this->schema->maxProperties)) { 347 | if (count(get_object_vars($this->data)) > $this->schema->maxProperties) { 348 | $this->fail(self::OBJECT_PROPERTIES_MAXIMUM, "", "/minProperties", ($this->schema->maxProperties == 1) ? "Object must have at most one defined property" : "Object must have at most {$this->schema->maxProperties} defined properties"); 349 | } 350 | } 351 | } 352 | 353 | 354 | private function checkArray() 355 | { 356 | if (!is_array($this->data)) { 357 | return; 358 | } 359 | if (isset($this->schema->items)) { 360 | $items = $this->schema->items; 361 | if (is_array($items)) { 362 | foreach ($this->data as $index => &$subData) { 363 | if (!is_numeric($index)) { 364 | throw new Exception("Arrays must only be numerically-indexed"); 365 | } 366 | if (isset($items[$index])) { 367 | $subResult = $this->subResult($subData, $items[$index]); 368 | $this->includeSubResult($subResult, "/{$index}", "/items/{$index}"); 369 | } else if (isset($this->schema->additionalItems)) { 370 | $additionalItems = $this->schema->additionalItems; 371 | if (!$additionalItems) { 372 | $this->fail(self::ARRAY_ADDITIONAL_ITEMS, "/{$index}", "/additionalItems", "Additional items (index " . count($items) . " or more) are not allowed"); 373 | } else if ($additionalItems !== TRUE) { 374 | $subResult = $this->subResult($subData, $additionalItems); 375 | $this->includeSubResult($subResult, "/{$index}", "/additionalItems"); 376 | } 377 | } 378 | } 379 | } else { 380 | foreach ($this->data as $index => &$subData) { 381 | if (!is_numeric($index)) { 382 | throw new Exception("Arrays must only be numerically-indexed"); 383 | } 384 | $subResult = $this->subResult($subData, $items); 385 | $this->includeSubResult($subResult, "/{$index}", "/items"); 386 | } 387 | } 388 | } 389 | if (isset($this->schema->minItems)) { 390 | if (count($this->data) < $this->schema->minItems) { 391 | $this->fail(self::ARRAY_LENGTH_SHORT, "", "/minItems", "Array is too short (must have at least {$this->schema->minItems} items)"); 392 | } 393 | } 394 | if (isset($this->schema->maxItems)) { 395 | if (count($this->data) > $this->schema->maxItems) { 396 | $this->fail(self::ARRAY_LENGTH_LONG, "", "/maxItems", "Array is too long (must have at most {$this->schema->maxItems} items)"); 397 | } 398 | } 399 | if (isset($this->schema->uniqueItems)) { 400 | foreach ($this->data as $indexA => $itemA) { 401 | foreach ($this->data as $indexB => $itemB) { 402 | if ($indexA < $indexB) { 403 | if (self::recursiveEqual($itemA, $itemB)) { 404 | $this->fail(self::ARRAY_UNIQUE, "", "/uniqueItems", "Array items must be unique (items $indexA and $indexB)"); 405 | break 2; 406 | } 407 | } 408 | } 409 | } 410 | } 411 | } 412 | 413 | 414 | private function checkString() 415 | { 416 | if (!is_string($this->data)) { 417 | return; 418 | } 419 | if (isset($this->schema->minLength)) { 420 | if (mb_strlen($this->data) < $this->schema->minLength) { 421 | $this->fail(self::STRING_LENGTH_SHORT, "", "/minLength", "String must be at least {$this->schema->minLength} characters long"); 422 | } 423 | } 424 | if (isset($this->schema->maxLength)) { 425 | if (mb_strlen($this->data) > $this->schema->maxLength) { 426 | $this->fail(self::STRING_LENGTH_LONG, "", "/maxLength", "String must be at most {$this->schema->maxLength} characters long"); 427 | } 428 | } 429 | if (isset($this->schema->pattern)) { 430 | $pattern = $this->schema->pattern; 431 | $patternFlags = isset($this->schema->patternFlags) ? $this->schema->patternFlags : ''; 432 | $result = preg_match("/" . str_replace("/", "\\/", $pattern) . "/" . $patternFlags, $this->data); 433 | if ($result === 0) { 434 | $this->fail(self::STRING_PATTERN, "", "/pattern", "String does not match pattern: $pattern"); 435 | } 436 | } 437 | } 438 | 439 | 440 | private function checkNumber() 441 | { 442 | if (is_string($this->data) || !is_numeric($this->data)) { 443 | return; 444 | } 445 | if (isset($this->schema->multipleOf)) { 446 | if (fmod($this->data / $this->schema->multipleOf, 1) != 0) { 447 | $this->fail(self::NUMBER_MULTIPLE_OF, "", "/multipleOf", "Number must be a multiple of {$this->schema->multipleOf}"); 448 | } 449 | } 450 | if (isset($this->schema->minimum)) { 451 | $minimum = $this->schema->minimum; 452 | if (isset($this->schema->exclusiveMinimum) && $this->schema->exclusiveMinimum) { 453 | if ($this->data <= $minimum) { 454 | $this->fail(self::NUMBER_MINIMUM_EXCLUSIVE, "", "", "Number must be > $minimum"); 455 | } 456 | } else { 457 | if ($this->data < $minimum) { 458 | $this->fail(self::NUMBER_MINIMUM, "", "/minimum", "Number must be >= $minimum"); 459 | } 460 | } 461 | } 462 | if (isset($this->schema->maximum)) { 463 | $maximum = $this->schema->maximum; 464 | if (isset($this->schema->exclusiveMaximum) && $this->schema->exclusiveMaximum) { 465 | if ($this->data >= $maximum) { 466 | $this->fail(self::NUMBER_MAXIMUM_EXCLUSIVE, "", "", "Number must be < $maximum"); 467 | } 468 | } else { 469 | if ($this->data > $maximum) { 470 | $this->fail(self::NUMBER_MAXIMUM, "", "/maximum", "Number must be <= $maximum"); 471 | } 472 | } 473 | } 474 | } 475 | 476 | 477 | private function checkComposite() 478 | { 479 | if (isset($this->schema->allOf)) { 480 | foreach ($this->schema->allOf as $index => $subSchema) { 481 | $subResult = $this->subResult($this->data, $subSchema, FALSE); 482 | $this->includeSubResult($subResult, "", "/allOf/" . (int) $index); 483 | } 484 | } 485 | if (isset($this->schema->anyOf)) { 486 | $failResults = []; 487 | foreach ($this->schema->anyOf as $index => $subSchema) { 488 | $subResult = $this->subResult($this->data, $subSchema, FALSE); 489 | if ($subResult->valid) { 490 | return; 491 | } 492 | $failResults[] = $subResult; 493 | } 494 | $this->fail(self::ANY_OF_MISSING, "", "/anyOf", "Value must satisfy at least one of the options", $failResults); 495 | } 496 | if (isset($this->schema->oneOf)) { 497 | $failResults = []; 498 | $successIndex = NULL; 499 | foreach ($this->schema->oneOf as $index => $subSchema) { 500 | $subResult = $this->subResult($this->data, $subSchema, FALSE); 501 | if ($subResult->valid) { 502 | if ($successIndex === NULL) { 503 | $successIndex = $index; 504 | } else { 505 | $this->fail(self::ONE_OF_MULTIPLE, "", "/oneOf", "Value satisfies more than one of the options ($successIndex and $index)"); 506 | } 507 | continue; 508 | } 509 | $failResults[] = $subResult; 510 | } 511 | if ($successIndex === NULL) { 512 | $this->fail(self::ONE_OF_MISSING, "", "/oneOf", "Value must satisfy one of the options", $failResults); 513 | } 514 | } 515 | if (isset($this->schema->not)) { 516 | $subResult = $this->subResult($this->data, $this->schema->not, FALSE); 517 | if ($subResult->valid) { 518 | $this->fail(self::NOT_PASSED, "", "/not", "Value satisfies prohibited schema"); 519 | } 520 | } 521 | } 522 | 523 | 524 | private function createValueForProperty($key) 525 | { 526 | $schema = NULL; 527 | if (isset($this->schema->properties->$key)) { 528 | $schema = $this->schema->properties->$key; 529 | } else if (isset($this->schema->patternProperties)) { 530 | foreach ($this->schema->patternProperties as $pattern => $subSchema) { 531 | if (preg_match("/" . str_replace("/", "\\/", $pattern) . "/", $key)) { 532 | $schema = $subSchema; 533 | break; 534 | } 535 | } 536 | } 537 | if (!$schema && isset($this->schema->additionalProperties)) { 538 | $schema = $this->schema->additionalProperties; 539 | } 540 | if ($schema) { 541 | if (isset($schema->default)) { 542 | $this->data->$key = unserialize(serialize($schema->default)); 543 | return TRUE; 544 | } 545 | if (isset($schema->type)) { 546 | $types = is_array($schema->type) ? $schema->type : array($schema->type); 547 | if (in_array("null", $types)) { 548 | $this->data->$key = NULL; 549 | } elseif (in_array("boolean", $types)) { 550 | $this->data->$key = TRUE; 551 | } elseif (in_array("integer", $types) || in_array("number", $types)) { 552 | $this->data->$key = 0; 553 | } elseif (in_array("string", $types)) { 554 | $this->data->$key = ""; 555 | } elseif (in_array("object", $types)) { 556 | $this->data->$key = new \StdClass; 557 | } elseif (in_array("array", $types)) { 558 | $this->data->$key = []; 559 | } else { 560 | return FALSE; 561 | } 562 | } 563 | return TRUE; 564 | } 565 | return FALSE; 566 | } 567 | 568 | 569 | } 570 | --------------------------------------------------------------------------------