├── LICENSE ├── README.md ├── composer.json ├── example ├── book.json └── car.json ├── src └── Json │ └── Validator.php └── tests ├── JsonValidatorTest.php ├── bootstrap.php ├── mock ├── additionalProperties.json ├── disallow.json ├── divisibleBy.json ├── empty.json ├── enum-array.json ├── enum-string.json ├── exclusiveMaximum.json ├── exclusiveMinimum.json ├── format │ ├── color.json │ ├── date-time.json │ ├── date.json │ ├── phone.json │ ├── style.json │ ├── time.json │ ├── uri.json │ └── utc-millisec.json ├── invalid-divisibleBy.json ├── invalid-enum.json ├── invalid-items.json ├── items-array.json ├── items-schema.json ├── maxItems.json ├── maxLength.json ├── maximum.json ├── minItems.json ├── minLength.json ├── minimum.json ├── missing-properties.json ├── missing-type.json ├── pattern.json ├── required.json ├── type │ ├── array.json │ ├── boolean.json │ ├── integer.json │ ├── multitype.json │ ├── null.json │ ├── number.json │ ├── object.json │ └── string.json └── uniqueItems.json └── phpunit.xml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2011 Harold Asbridge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This library provides JSON schema validation using the schema found at http://json-schema.org. 2 | Note that it is not yet feature complete, but does support basic validation. The JSON schema 3 | draft can be found at http://tools.ietf.org/html/draft-zyp-json-schema-03 4 | 5 | ## Requirements 6 | - PHP 5.3 or greater (requires namespace and closure support) 7 | 8 | ## Usage 9 | 10 | $someJson = '{"foo":"bar"}'; 11 | $jsonObject = json_decode($someJson); 12 | 13 | $validator = new JsonValidator('/path/to/yourschema.json'); 14 | 15 | $validator->validate($jsonObject); 16 | 17 | 18 | ## Supported Types 19 | 20 | Types may be defined as either a single string type name, or an array of allowable 21 | type names. 22 | 23 | - string 24 | - number 25 | - integer 26 | - boolean 27 | - object 28 | - array 29 | - null 30 | - any 31 | 32 | ## Supported Definitions 33 | 34 | Not all definitions are yet supported, but here is a list of those which are: 35 | 36 | - properties (object) 37 | - additionalProperties (object) 38 | - required (all) 39 | - pattern (string) 40 | - minLength (string) 41 | - maxLength (string) 42 | - format (string, number, integer) 43 | - minimum (number, integer) 44 | - maximum (number, integer) 45 | - exclusiveMinimum (number, integer) 46 | - exclusiveMaximum (number, integer) 47 | - divisibleBy (number, integer) 48 | - enum (array) 49 | - minItems (array) 50 | - maxItems (array) 51 | - uniqueItems (array) 52 | - items (array) 53 | - disallow (all) 54 | 55 | The following definitions are not yet supported: 56 | 57 | - patternProperties 58 | - dependencies 59 | - extends 60 | - id 61 | - $ref 62 | - $schema -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hasbridge/json-schema-validator", 3 | "type": "library", 4 | "description": "PHP 5.3 implementation of json schema validation", 5 | "homepage": "https://github.com/hasbridge/php-json-schema", 6 | "type": "library", 7 | "keywords": ["json", "schema"], 8 | "license": "MIT", 9 | "authors": [{ 10 | "name": "Harold Asbridge", 11 | "email": "hasbridge@gmail.com" 12 | }], 13 | "require": { 14 | "php": ">=5.3.2" 15 | }, 16 | "autoload": { 17 | "psr-0": { 18 | "Json": "src/" 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /example/book.json: -------------------------------------------------------------------------------- 1 | { 2 | "description" : "Book", 3 | "type" : "object", 4 | "properties" : { 5 | "name" : { 6 | "title" : "Book name", 7 | "type" : "string", 8 | "required" : true 9 | }, 10 | "author" : { 11 | "title" : "Book author name", 12 | "type" : "string", 13 | "required" : true 14 | }, 15 | "isbn" : { 16 | "title" : "Book ISBN", 17 | "type" : "string", 18 | "required" : true 19 | }, 20 | "publisher" : { 21 | "title" : "Book publisher", 22 | "type" : "string", 23 | "required" : true 24 | }, 25 | "price" : { 26 | "title" : "Book selling price", 27 | "type" : "number", 28 | "required" : true 29 | } 30 | }, 31 | "additionalProperties" : false 32 | } -------------------------------------------------------------------------------- /example/car.json: -------------------------------------------------------------------------------- 1 | { 2 | "description" : "Car", 3 | "type" : "object", 4 | "properties" : { 5 | "manufacturer" : { 6 | "type" : "string", 7 | "required" : true 8 | }, 9 | "model" : { 10 | "type" : "string", 11 | "required" : true 12 | }, 13 | "year" : { 14 | "type" : "integer", 15 | "required" : true, 16 | "minimum" : 2011, 17 | "maximum" : 2012 18 | }, 19 | "color" : { 20 | "type" : "string", 21 | "required" : true 22 | }, 23 | "alloys" : { 24 | "type" : "boolean", 25 | "required" : false 26 | }, 27 | "features" : { 28 | "type" : "array", 29 | "required" : true, 30 | "minItems" : 2, 31 | "maxItems" : 3, 32 | "uniqueItems" : true 33 | }, 34 | "id" : { 35 | "type" : "string", 36 | "required" : true, 37 | "pattern" : "/[A-Z]/", 38 | "minLength" : 4, 39 | "maxLength" : 5 40 | }, 41 | "engine" : { 42 | "type" : "object", 43 | "required" : true, 44 | "properties" : { 45 | "cylinders" : { 46 | "type" : "integer", 47 | "required" : "true" 48 | }, 49 | "displacement" : { 50 | "type" : "integer", 51 | "required" : "true" 52 | }, 53 | "horsepower" : { 54 | "type" : "number", 55 | "required" : true 56 | }, 57 | "torque" : { 58 | "type" : "number", 59 | "required" : true 60 | } 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/Json/Validator.php: -------------------------------------------------------------------------------- 1 | 14 | * @version 0.1 15 | */ 16 | class Validator 17 | { 18 | protected $schemaDefinition; 19 | 20 | /** 21 | * @var stdClass 22 | */ 23 | protected $schema; 24 | 25 | /** 26 | * Initialize validation object 27 | * 28 | * @param string $schemaFile 29 | */ 30 | public function __construct($schemaFile) 31 | { 32 | if (!file_exists($schemaFile)) { 33 | throw new SchemaException(sprintf('Schema file not found: [%s]', $schemaFile)); 34 | } 35 | $data = file_get_contents($schemaFile); 36 | $this->schema = json_decode($data); 37 | 38 | if ($this->schema === null) { 39 | throw new SchemaException('Unable to parse JSON data - syntax error?'); 40 | } 41 | 42 | // @TODO - validate schema itself 43 | } 44 | 45 | /** 46 | * Validate schema object 47 | * 48 | * @param mixed $entity 49 | * @param string $entityName 50 | * 51 | * @return Validator 52 | */ 53 | public function validate($entity, $entityName = null) 54 | { 55 | $entityName = $entityName ?: 'root'; 56 | 57 | // Validate root type 58 | $this->validateType($entity, $this->schema, $entityName); 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Check format restriction 65 | * 66 | * @param mixed $entity 67 | * @param object $schema 68 | * @param string $entityName 69 | * 70 | * @return Validator 71 | */ 72 | public function checkFormat($entity, $schema, $entityName) 73 | { 74 | if (!isset($schema->format)) { 75 | return $this; 76 | } 77 | 78 | $valid = true; 79 | switch ($schema->format) { 80 | case 'date-time': 81 | if (!preg_match('#^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$#', $entity)) { 82 | $valid = false; 83 | } 84 | break; 85 | case 'date': 86 | if (!preg_match('#^\d{4}-\d{2}-\d{2}$#', $entity)) { 87 | $valid = false; 88 | } 89 | break; 90 | case 'time': 91 | if (!preg_match('#^\d{2}:\d{2}:\d{2}$#', $entity)) { 92 | $valid = false; 93 | } 94 | break; 95 | case 'utc-millisec': 96 | if ($entity < 0) { 97 | $valid = false; 98 | } 99 | break; 100 | case 'color': 101 | if (!in_array($entity, array('maroon', 'red', 'orange', 102 | 'yellow', 'olive', 'green', 'purple', 'fuchsia', 'lime', 103 | 'teal', 'aqua', 'blue', 'navy', 'black', 'gray', 'silver', 'white'))) { 104 | if (!preg_match('#^\#[0-9A-F]{6}$#', $entity) && !preg_match('#^\#[0-9A-F]{3}$#', $entity)) { 105 | $valid = false; 106 | } 107 | } 108 | break; 109 | case 'style': 110 | if (!preg_match('#(\.*?)[ ]?:[ ]?(.*?)#', $entity)) { 111 | $valid = false; 112 | } 113 | break; 114 | case 'phone': 115 | if (!preg_match('#^[0-9\-+ \(\)]*$#', $entity)) { 116 | $valid = false; 117 | } 118 | break; 119 | case 'uri': 120 | if (!preg_match('#^[A-Za-z0-9:/;,\-_\?&\.%\+\|\#=]*$#', $entity)) { 121 | $valid = false; 122 | } 123 | break; 124 | } 125 | 126 | if (!$valid) { 127 | throw new ValidationException(sprintf('Value for [%s] must match format [%s]', $entityName, $schema->format)); 128 | } 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Validate object properties 135 | * 136 | * @param object $entity 137 | * @param object $schema 138 | * @param string $entityName 139 | * 140 | * @return Validator 141 | */ 142 | protected function validateProperties($entity, $schema, $entityName) 143 | { 144 | $properties = get_object_vars($entity); 145 | 146 | if (!isset($schema->properties)) { 147 | return $this; 148 | //throw new SchemaException(sprintf('No properties defined for [%s]', $entityName)); 149 | } 150 | 151 | // Check defined properties 152 | foreach($schema->properties as $propertyName => $property) { 153 | if (array_key_exists($propertyName, $properties)) { 154 | // Check type 155 | $path = $entityName . '.' . $propertyName; 156 | $this->validateType($entity->{$propertyName}, $property, $path); 157 | } else { 158 | // Check required 159 | if (isset($property->required) && $property->required) { 160 | throw new ValidationException(sprintf('Missing required property [%s] for [%s]', $propertyName, $entityName)); 161 | } 162 | } 163 | } 164 | 165 | // Check additional properties 166 | if (isset($schema->additionalProperties) && !$schema->additionalProperties) { 167 | $extra = array_diff(array_keys((array)$entity), array_keys((array)$schema->properties)); 168 | if (count($extra)) { 169 | throw new ValidationException(sprintf('Additional properties [%s] not allowed for property [%s]', implode(',', $extra), $entityName)); 170 | } 171 | } 172 | 173 | return $this; 174 | } 175 | 176 | /** 177 | * Validate entity type 178 | * 179 | * @param mixed $entity 180 | * @param object $schema 181 | * @param string $entityName 182 | * 183 | * @return Validator 184 | */ 185 | protected function validateType($entity, $schema, $entityName) 186 | { 187 | if (isset($schema->type)) { 188 | $types = $schema->type; 189 | } else { 190 | $types = 'any'; 191 | //throw new ValidationException(sprintf('No type given for [%s]', $entityName)); 192 | } 193 | 194 | if (!is_array($types)) { 195 | $types = array($types); 196 | } 197 | 198 | $valid = false; 199 | 200 | foreach ($types as $type) { 201 | switch ($type) { 202 | case 'object': 203 | if (is_object($entity)) { 204 | $this->checkTypeObject($entity, $schema, $entityName); 205 | $valid = true; 206 | } 207 | break; 208 | case 'string': 209 | if (is_string($entity)) { 210 | $this->checkTypeString($entity, $schema, $entityName); 211 | $valid = true; 212 | } 213 | break; 214 | case 'array': 215 | if (is_array($entity)) { 216 | $this->checkTypeArray($entity, $schema, $entityName); 217 | $valid = true; 218 | } 219 | break; 220 | case 'integer': 221 | if (!is_string($entity) && is_int($entity)) { 222 | $this->checkTypeInteger($entity, $schema, $entityName); 223 | $valid = true; 224 | } 225 | break; 226 | case 'number': 227 | if (!is_string($entity) && is_numeric($entity)) { 228 | $this->checkTypeNumber($entity, $schema, $entityName); 229 | $valid = true; 230 | } 231 | break; 232 | case 'boolean': 233 | if (!is_string($entity) && is_bool($entity)) { 234 | $this->checkTypeBoolean($entity, $schema, $entityName); 235 | $valid = true; 236 | } 237 | break; 238 | case 'null': 239 | if (is_null($entity)) { 240 | $this->checkTypeNull($entity, $schema, $entityName); 241 | $valid = true; 242 | } 243 | break; 244 | case 'any': 245 | $this->checkTypeAny($entity, $schema, $entityName); 246 | $valid = true; 247 | break; 248 | default: 249 | // Do nothing 250 | $valid = true; 251 | break; 252 | } 253 | } 254 | 255 | if (!$valid) { 256 | throw new ValidationException(sprintf('Property [%s] must be one of the following types: [%s]', $entityName, implode(', ', $types))); 257 | } 258 | 259 | return $this; 260 | } 261 | 262 | 263 | /** 264 | * Check object type 265 | * 266 | * @param mixed $entity 267 | * @param object $schema 268 | * @param string $entityName 269 | * 270 | * @return Validator 271 | */ 272 | protected function checkTypeObject($entity, $schema, $entityName) 273 | { 274 | $this->validateProperties($entity, $schema, $entityName); 275 | 276 | return $this; 277 | } 278 | 279 | /** 280 | * Check number type 281 | * 282 | * @param mixed $entity 283 | * @param object $schema 284 | * @param string $entityName 285 | * 286 | * @return Validator 287 | */ 288 | protected function checkTypeNumber($entity, $schema, $entityName) 289 | { 290 | $this->checkMinimum($entity, $schema, $entityName); 291 | $this->checkMaximum($entity, $schema, $entityName); 292 | $this->checkExclusiveMinimum($entity, $schema, $entityName); 293 | $this->checkExclusiveMaximum($entity, $schema, $entityName); 294 | $this->checkFormat($entity, $schema, $entityName); 295 | $this->checkEnum($entity, $schema, $entityName); 296 | $this->checkDisallow($entity, $schema, $entityName); 297 | $this->checkDivisibleBy($entity, $schema, $entityName); 298 | 299 | return $this; 300 | } 301 | 302 | /** 303 | * Check integer type 304 | * 305 | * @param mixed $entity 306 | * @param object $schema 307 | * @param string $entityName 308 | * 309 | * @return Validator 310 | */ 311 | protected function checkTypeInteger($entity, $schema, $entityName) 312 | { 313 | $this->checkMinimum($entity, $schema, $entityName); 314 | $this->checkMaximum($entity, $schema, $entityName); 315 | $this->checkExclusiveMinimum($entity, $schema, $entityName); 316 | $this->checkExclusiveMaximum($entity, $schema, $entityName); 317 | $this->checkFormat($entity, $schema, $entityName); 318 | $this->checkEnum($entity, $schema, $entityName); 319 | $this->checkDisallow($entity, $schema, $entityName); 320 | $this->checkDivisibleBy($entity, $schema, $entityName); 321 | 322 | return $this; 323 | } 324 | 325 | /** 326 | * Check boolean type 327 | * 328 | * @param mixed $entity 329 | * @param object $schema 330 | * @param string $entityName 331 | * 332 | * @return Validator 333 | */ 334 | protected function checkTypeBoolean($entity, $schema, $entityName) 335 | { 336 | return $this; 337 | } 338 | 339 | /** 340 | * Check string type 341 | * 342 | * @param mixed $entity 343 | * @param object $schema 344 | * @param string $entityName 345 | * 346 | * @return Validator 347 | */ 348 | protected function checkTypeString($entity, $schema, $entityName) 349 | { 350 | $this->checkPattern($entity, $schema, $entityName); 351 | $this->checkMinLength($entity, $schema, $entityName); 352 | $this->checkMaxLength($entity, $schema, $entityName); 353 | $this->checkFormat($entity, $schema, $entityName); 354 | $this->checkEnum($entity, $schema, $entityName); 355 | $this->checkDisallow($entity, $schema, $entityName); 356 | 357 | return $this; 358 | } 359 | 360 | /** 361 | * Check array type 362 | * 363 | * @param mixed $entity 364 | * @param object $schema 365 | * @param string $entityName 366 | * 367 | * @return Validator 368 | */ 369 | protected function checkTypeArray($entity, $schema, $entityName) 370 | { 371 | $this->checkMinItems($entity, $schema, $entityName); 372 | $this->checkMaxItems($entity, $schema, $entityName); 373 | $this->checkUniqueItems($entity, $schema, $entityName); 374 | $this->checkEnum($entity, $schema, $entityName); 375 | $this->checkItems($entity, $schema, $entityName); 376 | $this->checkDisallow($entity, $schema, $entityName); 377 | 378 | return $this; 379 | } 380 | 381 | /** 382 | * Check null type 383 | * 384 | * @param mixed $entity 385 | * @param object $schema 386 | * @param string $entityName 387 | * 388 | * @return Validator 389 | */ 390 | protected function checkTypeNull($entity, $schema, $entityName) 391 | { 392 | return $this; 393 | } 394 | 395 | /** 396 | * Check any type 397 | * 398 | * @param mixed $entity 399 | * @param object $schema 400 | * @param string $entityName 401 | * 402 | * @return Validator 403 | */ 404 | protected function checkTypeAny($entity, $schema, $entityName) 405 | { 406 | $this->checkDisallow($entity, $schema, $entityName); 407 | 408 | return $this; 409 | } 410 | 411 | /** 412 | * Check minimum value 413 | * 414 | * @param int|float $entity 415 | * @param object $schema 416 | * @param string $entityName 417 | * 418 | * @return Validator 419 | */ 420 | protected function checkMinimum($entity, $schema, $entityName) 421 | { 422 | if (isset($schema->minimum)) { 423 | if ($entity < $schema->minimum) { 424 | throw new ValidationException(sprintf('Invalid value for [%s], minimum is [%s]', $entityName, $schema->minimum)); 425 | } 426 | } 427 | 428 | return $this; 429 | } 430 | 431 | /** 432 | * Check maximum value 433 | * 434 | * @param int|float $entity 435 | * @param object $schema 436 | * @param string $entityName 437 | * 438 | * @return Validator 439 | */ 440 | protected function checkMaximum($entity, $schema, $entityName) 441 | { 442 | if (isset($schema->maximum)) { 443 | if ($entity > $schema->maximum) { 444 | throw new ValidationException(sprintf('Invalid value for [%s], maximum is [%s]', $entityName, $schema->maximum)); 445 | } 446 | } 447 | 448 | return $this; 449 | } 450 | 451 | /** 452 | * Check exlusive minimum requirement 453 | * 454 | * @param int|float $entity 455 | * @param object $schema 456 | * @param string $entityName 457 | * 458 | * @return Validator 459 | */ 460 | protected function checkExclusiveMinimum($entity, $schema, $entityName) 461 | { 462 | if (isset($schema->minimum) && isset($schema->exclusiveMinimum) && $schema->exclusiveMinimum) { 463 | if ($entity == $schema->minimum) { 464 | throw new ValidationException(sprintf('Invalid value for [%s], must be greater than [%s]', $entityName, $schema->minimum)); 465 | } 466 | } 467 | 468 | return $this; 469 | } 470 | 471 | /** 472 | * Check exclusive maximum requirement 473 | * 474 | * @param int|float $entity 475 | * @param object $schema 476 | * @param string $entityName 477 | * 478 | * @return Validator 479 | */ 480 | protected function checkExclusiveMaximum($entity, $schema, $entityName) 481 | { 482 | if (isset($schema->maximum) && isset($schema->exclusiveMaximum) && $schema->exclusiveMaximum) { 483 | if ($entity == $schema->maximum) { 484 | throw new ValidationException(sprintf('Invalid value for [%s], must be less than [%s]', $entityName, $schema->maximum)); 485 | } 486 | } 487 | 488 | return $this; 489 | } 490 | 491 | /** 492 | * Check value against regex pattern 493 | * 494 | * @param string $entity 495 | * @param object $schema 496 | * @param string $entityName 497 | * 498 | * @return Validator 499 | */ 500 | protected function checkPattern($entity, $schema, $entityName) 501 | { 502 | if (isset($schema->pattern) && $schema->pattern) { 503 | if (!preg_match($schema->pattern, $entity)) { 504 | throw new ValidationException(sprintf('String does not match pattern for [%s]', $entityName)); 505 | } 506 | } 507 | 508 | return $this; 509 | } 510 | 511 | /** 512 | * Check string minimum length 513 | * 514 | * @param string $entity 515 | * @param object $schema 516 | * @param string $entityName 517 | * 518 | * @return Validator 519 | */ 520 | protected function checkMinLength($entity, $schema, $entityName) 521 | { 522 | if (isset($schema->minLength) && $schema->minLength) { 523 | if (strlen($entity) < $schema->minLength) { 524 | throw new ValidationException(sprintf('String too short for [%s], minimum length is [%s]', $entityName, $schema->minLength)); 525 | } 526 | } 527 | 528 | return $this; 529 | } 530 | 531 | /** 532 | * Check string maximum length 533 | * 534 | * @param string $entity 535 | * @param object $schema 536 | * @param string $entityName 537 | * 538 | * @return Validator 539 | */ 540 | protected function checkMaxLength($entity, $schema, $entityName) 541 | { 542 | if (isset($schema->maxLength) && $schema->maxLength) { 543 | if (strlen($entity) > $schema->maxLength) { 544 | throw new ValidationException(sprintf('String too long for [%s], maximum length is [%s]', $entityName, $schema->maxLength)); 545 | } 546 | } 547 | 548 | return $this; 549 | } 550 | 551 | /** 552 | * Check array minimum items 553 | * 554 | * @param array $entity 555 | * @param object $schema 556 | * @param string $entityName 557 | * 558 | * @return Validator 559 | */ 560 | protected function checkMinItems($entity, $schema, $entityName) 561 | { 562 | if (isset($schema->minItems) && $schema->minItems) { 563 | if (count($entity) < $schema->minItems) { 564 | throw new ValidationException(sprintf('Not enough array items for [%s], minimum is [%s]', $entityName, $schema->minItems)); 565 | } 566 | } 567 | 568 | return $this; 569 | } 570 | 571 | /** 572 | * Check array maximum items 573 | * 574 | * @param array $entity 575 | * @param object $schema 576 | * @param string $entityName 577 | * 578 | * @return Validator 579 | */ 580 | protected function checkMaxItems($entity, $schema, $entityName) 581 | { 582 | if (isset($schema->maxItems) && $schema->maxItems) { 583 | if (count($entity) > $schema->maxItems) { 584 | throw new ValidationException(sprintf('Too many array items for [%s], maximum is [%s]', $entityName, $schema->maxItems)); 585 | } 586 | } 587 | 588 | return $this; 589 | } 590 | 591 | /** 592 | * Check array unique items 593 | * 594 | * @param array $entity 595 | * @param object $schema 596 | * @param string $entityName 597 | * 598 | * @return Validator 599 | */ 600 | protected function checkUniqueItems($entity, $schema, $entityName) 601 | { 602 | if (isset($schema->uniqueItems) && $schema->uniqueItems) { 603 | if (count(array_unique($entity)) != count($entity)) { 604 | throw new ValidationException(sprintf('All items in array [%s] must be unique', $entityName)); 605 | } 606 | } 607 | 608 | return $this; 609 | } 610 | 611 | /** 612 | * Check enum restriction 613 | * 614 | * @param array $entity 615 | * @param object $schema 616 | * @param string $entityName 617 | * 618 | * @return Validator 619 | */ 620 | protected function checkEnum($entity, $schema, $entityName) 621 | { 622 | $valid = true; 623 | if (isset($schema->enum) && $schema->enum) { 624 | if (!is_array($schema->enum)) { 625 | throw new SchemaException(sprintf('Enum property must be an array for [%s]', $entityName)); 626 | } 627 | if (is_array($entity)) { 628 | foreach ($entity as $val) { 629 | if (!in_array($val, $schema->enum)) { 630 | $valid = false; 631 | } 632 | } 633 | } else { 634 | if (!in_array($entity, $schema->enum)) { 635 | $valid = false; 636 | } 637 | } 638 | } 639 | 640 | if (!$valid) { 641 | throw new ValidationException(sprintf('Invalid value(s) for [%s], allowable values are [%s]', $entityName, implode(',', $schema->enum))); 642 | } 643 | 644 | return $this; 645 | } 646 | 647 | /** 648 | * Check items restriction 649 | * 650 | * @param array $entity 651 | * @param object $schema 652 | * @param string $entityName 653 | * 654 | * @return Validator 655 | */ 656 | protected function checkItems($entity, $schema, $entityName) 657 | { 658 | if (isset($schema->items) && $schema->items) { 659 | // Item restriction is an array of schemas 660 | if (is_array($schema->items)) { 661 | foreach($entity as $index => $node) { 662 | $nodeEntityName = $entityName . '[' . $index . ']'; 663 | 664 | // Check if the item passes any of the item validations 665 | foreach($schema->items as $item) { 666 | $nodeValid = true; 667 | try { 668 | $this->validateType($node, $item, $nodeEntityName); 669 | // Pass 670 | break; 671 | } catch (ValidationException $e) { 672 | $nodeValid = false; 673 | } 674 | } 675 | 676 | // If item did not pass any item validations 677 | if (!$nodeValid) { 678 | $allowedTypes = array_map(function($item){ 679 | return $item->type == 'object' ? 'object (schema)' : $item->type; 680 | }, $schema->items); 681 | throw new ValidationException(sprintf('Invalid value for [%s], must be one of the following types: [%s]', 682 | $nodeEntityName, implode(', ' , $allowedTypes))); 683 | } 684 | } 685 | // Item restriction is a single schema 686 | } else if (is_object($schema->items)) { 687 | foreach($entity as $index => $node) { 688 | $nodeEntityName = $entityName . '[' . $index . ']'; 689 | $this->validateType($node, $schema->items, $nodeEntityName); 690 | } 691 | 692 | } else { 693 | throw new SchemaException(sprintf('Invalid items value for [%s]', $entityName)); 694 | } 695 | } 696 | 697 | return $this; 698 | } 699 | 700 | /** 701 | * Check disallowed entity type 702 | * 703 | * @param mixed $entity 704 | * @param object $schema 705 | * @param string $entityName 706 | * 707 | * @return Validator 708 | */ 709 | protected function checkDisallow($entity, $schema, $entityName) 710 | { 711 | if (isset($schema->disallow) && $schema->disallow) { 712 | $thisSchema = clone $schema; 713 | $thisSchema->type = $schema->disallow; 714 | unset($thisSchema->disallow); 715 | 716 | // We are expecting an exception - if one is not thrown, 717 | // then we have a matching disallowed type 718 | try { 719 | $valid = false; 720 | $this->validateType($entity, $thisSchema, $entityName); 721 | } catch (ValidationException $e) { 722 | $valid = true; 723 | } 724 | if (!$valid) { 725 | $disallowedTypes = array_map(function($item){ 726 | return is_object($item) ? 'object (schema)' : $item; 727 | }, is_array($schema->disallow) ? $schema->disallow : array($schema->disallow)); 728 | throw new ValidationException(sprintf('Invalid value for [%s], disallowed types are [%s]', 729 | $entityName, implode(', ', $disallowedTypes))); 730 | } 731 | } 732 | 733 | return $this; 734 | } 735 | 736 | /** 737 | * Check divisibleby restriction 738 | * 739 | * @param int|float $entity 740 | * @param object $schema 741 | * @param string $entityName 742 | * 743 | * @return Validator 744 | */ 745 | protected function checkDivisibleBy($entity, $schema, $entityName) 746 | { 747 | if (isset($schema->divisibleBy) && $schema->divisibleBy) { 748 | if (!is_numeric($schema->divisibleBy)) { 749 | throw new SchemaException(sprintf('Invalid divisibleBy value for [%s], must be numeric', $entityName)); 750 | } 751 | 752 | if ($entity % $schema->divisibleBy != 0) { 753 | throw new ValidationException(sprintf('Invalid value for [%s], must be divisible by [%d]', $entityName, $schema->divisibleBy)); 754 | } 755 | } 756 | 757 | return $this; 758 | } 759 | } 760 | -------------------------------------------------------------------------------- /tests/JsonValidatorTest.php: -------------------------------------------------------------------------------- 1 | stringProp = "AB"; 19 | $o->arrayProp = array('foo', 'bar'); 20 | $o->numberProp = 1.1; 21 | $o->integerProp = 2; 22 | $o->booleanProp = false; 23 | $o->nullProp = null; 24 | $o->anyProp = 1; 25 | $o->multiProp = "foo"; 26 | $o->customProp = 'asdf'; 27 | 28 | $o->dateTimeFormatProp = '2011-12-14T09:06:00Z'; 29 | $o->dateFormatProp = '2011-12-14'; 30 | $o->timeFormatProp = "09:00:00"; 31 | $o->utcMillisecFormatProp = 123456789; 32 | $o->colorFormatProp = "#000000"; 33 | $o->styleFormatProp = "background: #FFF url('foo.png') no-repeat 0px 0px;"; 34 | $o->phoneFormatProp = "555-555-1234"; 35 | $o->uriFormatProp = "https://www.google.com/"; 36 | 37 | $o->objectProp = new stdClass(); 38 | $o->objectProp->foo = 'bar'; 39 | 40 | return $o; 41 | } 42 | 43 | /** 44 | * Get validator object 45 | * 46 | * @param string $schemaFile 47 | * 48 | * @return JsonValidator 49 | */ 50 | protected function getValidator($schemaFile) 51 | { 52 | return new Validator(TEST_DIR . '/mock/' . $schemaFile); 53 | } 54 | 55 | /** 56 | * @expectedException Json\SchemaException 57 | */ 58 | public function testSchemaNotFound() 59 | { 60 | $v = new Validator('asdf'); 61 | } 62 | 63 | /** 64 | * @expectedException Json\SchemaException 65 | */ 66 | public function testInvalidSchema() 67 | { 68 | $v = new Validator(TEST_DIR . '/mock/empty.json'); 69 | } 70 | 71 | public function testMissingProperties() 72 | { 73 | $v = $this->getValidator('missing-properties.json'); 74 | 75 | $o = (object)array( 76 | 'foo' => 'bar' 77 | ); 78 | 79 | $v->validate($o); 80 | } 81 | 82 | public function testMissingType() 83 | { 84 | $v = new Validator(TEST_DIR . '/mock/missing-type.json'); 85 | 86 | $o = (object)array( 87 | 'foo' => 'bar' 88 | ); 89 | 90 | $v->validate($o); 91 | } 92 | 93 | /** 94 | * Test multiple types for property 95 | */ 96 | public function testMultiProp() 97 | { 98 | $v = $this->getValidator('type/multitype.json'); 99 | $v->validate("asdf"); 100 | $v->validate(1234); 101 | } 102 | 103 | /** 104 | * @expectedException Json\ValidationException 105 | */ 106 | public function testMissingRequired() 107 | { 108 | $v = $this->getValidator('required.json'); 109 | $o = (object)array( 110 | 'baz' => 'bar' 111 | ); 112 | $v->validate($o); 113 | } 114 | 115 | /** 116 | * @expectedException Json\ValidationException 117 | */ 118 | public function testInvalidAdditionalProperties() 119 | { 120 | $v = $this->getValidator('additionalProperties.json'); 121 | $o = (object)array( 122 | 'foo' => 'bar' 123 | ); 124 | $v->validate($o); 125 | } 126 | 127 | public function testString() 128 | { 129 | $v = $this->getValidator('type/string.json'); 130 | $v->validate('foo'); 131 | } 132 | 133 | /** 134 | * @expectedException Json\ValidationException 135 | */ 136 | public function testInvalidString() 137 | { 138 | $v = $this->getValidator('type/string.json'); 139 | $v->validate(1234); 140 | } 141 | 142 | public function testNumber() 143 | { 144 | $v = $this->getValidator('type/number.json'); 145 | $v->validate(1); 146 | $v->validate(1.1); 147 | } 148 | 149 | /** 150 | * @expectedException Json\ValidationException 151 | */ 152 | public function testInvalidNumber() 153 | { 154 | $v = $this->getValidator('type/number.json'); 155 | $v->validate('asdf'); 156 | } 157 | 158 | public function testInteger() 159 | { 160 | $v = $this->getValidator('type/integer.json'); 161 | $v->validate(1); 162 | } 163 | 164 | /** 165 | * @expectedException Json\ValidationException 166 | */ 167 | public function testInvalidInteger() 168 | { 169 | $v = $this->getValidator('type/integer.json'); 170 | $v->validate('asdf'); 171 | } 172 | 173 | public function testBoolean() 174 | { 175 | $v = $this->getValidator('type/boolean.json'); 176 | $v->validate(true); 177 | $v->validate(false); 178 | } 179 | 180 | /** 181 | * @expectedException Json\ValidationException 182 | */ 183 | public function testInvalidBoolean() 184 | { 185 | $v = $this->getValidator('type/boolean.json'); 186 | $v->validate('asdf'); 187 | } 188 | 189 | public function testArray() 190 | { 191 | $v = $this->getValidator('type/array.json'); 192 | $v->validate(array(1, 2, 3)); 193 | $v->validate(array()); 194 | } 195 | 196 | /** 197 | * @expectedException Json\ValidationException 198 | */ 199 | public function testInvalidArray() 200 | { 201 | $v = $this->getValidator('type/array.json'); 202 | $v->validate('asdf'); 203 | } 204 | 205 | public function testNull() 206 | { 207 | $v = $this->getValidator('type/null.json'); 208 | $v->validate(null); 209 | } 210 | 211 | /** 212 | * @expectedException Json\ValidationException 213 | */ 214 | public function testInvalidNull() 215 | { 216 | $v = $this->getValidator('type/null.json'); 217 | $v->validate(1234); 218 | } 219 | 220 | public function testObject() 221 | { 222 | $v = $this->getValidator('type/object.json'); 223 | $o = new stdClass(); 224 | $v->validate($o); 225 | } 226 | 227 | /** 228 | * @expectedException Json\ValidationException 229 | */ 230 | public function testInvalidObject() 231 | { 232 | $v = $this->getValidator('type/object.json'); 233 | $v->validate('asdf'); 234 | } 235 | 236 | public function testMinimum() 237 | { 238 | $v = $this->getValidator('minimum.json'); 239 | $v->validate(1); 240 | } 241 | 242 | /** 243 | * @expectedException Json\ValidationException 244 | */ 245 | public function testInvalidMinimum() 246 | { 247 | $v = $this->getValidator('minimum.json'); 248 | $v->validate(0); 249 | } 250 | 251 | public function testMaximum() 252 | { 253 | $v = $this->getValidator('maximum.json'); 254 | $v->validate(1); 255 | } 256 | 257 | /** 258 | * @expectedException Json\ValidationException 259 | */ 260 | public function testInvalidMaximum() 261 | { 262 | $v = $this->getValidator('maximum.json'); 263 | $v->validate(3); 264 | } 265 | 266 | public function testExclusiveMinimum() 267 | { 268 | $v = $this->getValidator('exclusiveMinimum.json'); 269 | $v->validate(2); 270 | } 271 | 272 | /** 273 | * @expectedException Json\ValidationException 274 | */ 275 | public function testInvalidExclusiveMinimum() 276 | { 277 | $v = $this->getValidator('exclusiveMinimum.json'); 278 | $v->validate(1); 279 | } 280 | 281 | public function testExclusiveMaximum() 282 | { 283 | $v = $this->getValidator('exclusiveMaximum.json'); 284 | $v->validate(1); 285 | } 286 | 287 | /** 288 | * @expectedException Json\ValidationException 289 | */ 290 | public function testInvalidExclusiveMaximum() 291 | { 292 | $v = $this->getValidator('exclusiveMaximum.json'); 293 | $v->validate(2); 294 | } 295 | 296 | public function testPattern() 297 | { 298 | $v = $this->getValidator('pattern.json'); 299 | $o = "ASDF"; 300 | $v->validate($o); 301 | } 302 | 303 | /** 304 | * @expectedException Json\ValidationException 305 | */ 306 | public function testInvalidPattern() 307 | { 308 | $v = $this->getValidator('pattern.json'); 309 | $o = "asdf"; 310 | $v->validate($o); 311 | } 312 | 313 | public function testMinLength() 314 | { 315 | $v = $this->getValidator('minLength.json'); 316 | $o = "foo"; 317 | $v->validate($o); 318 | } 319 | 320 | /** 321 | * @expectedException Json\ValidationException 322 | */ 323 | public function testInvalidMinLength() 324 | { 325 | $v = $this->getValidator('minLength.json'); 326 | $o = "a"; 327 | $v->validate($o); 328 | } 329 | 330 | public function testMaxLength() 331 | { 332 | $v = $this->getValidator('maxLength.json'); 333 | $o = "foo"; 334 | $v->validate($o); 335 | } 336 | 337 | /** 338 | * @expectedException Json\ValidationException 339 | */ 340 | public function testInvalidMaxLength() 341 | { 342 | $v = $this->getValidator('maxLength.json'); 343 | $o = "foo bar"; 344 | $v->validate($o); 345 | } 346 | 347 | public function testMinItems() 348 | { 349 | $v = $this->getValidator('minItems.json'); 350 | $o = array('foo', 'bar'); 351 | $v->validate($o); 352 | } 353 | 354 | /** 355 | * @expectedException Json\ValidationException 356 | */ 357 | public function testInvalidMinItems() 358 | { 359 | $v = $this->getValidator('minItems.json'); 360 | $o = array('foo'); 361 | $v->validate($o); 362 | } 363 | 364 | public function testMaxItems() 365 | { 366 | $v = $this->getValidator('maxItems.json'); 367 | $o = array('foo', 'bar'); 368 | $v->validate($o); 369 | } 370 | 371 | /** 372 | * @expectedException Json\ValidationException 373 | */ 374 | public function testInvalidMaxItems() 375 | { 376 | $v = $this->getValidator('maxItems.json'); 377 | $o = array('foo', 'bar', 'baz'); 378 | $v->validate($o); 379 | } 380 | 381 | public function testUniqueItems() 382 | { 383 | $v = $this->getValidator('uniqueItems.json'); 384 | $o = array('foo', 'bar'); 385 | $v->validate($o); 386 | } 387 | 388 | /** 389 | * @expectedException Json\ValidationException 390 | */ 391 | public function testInvalidUniqueItems() 392 | { 393 | $v = $this->getValidator('uniqueItems.json'); 394 | $o = array('foo', 'foo'); 395 | $v->validate($o); 396 | } 397 | 398 | public function testEnumArray() 399 | { 400 | $v = $this->getValidator('enum-array.json'); 401 | $o = array('foo', 'bar'); 402 | $v->validate($o); 403 | } 404 | 405 | /** 406 | * @expectedException Json\ValidationException 407 | */ 408 | public function testInvalidEnumArray() 409 | { 410 | $v = $this->getValidator('enum-array.json'); 411 | $o = array('foo', 'baz'); 412 | $v->validate($o); 413 | } 414 | 415 | public function testEnumString() 416 | { 417 | $v = $this->getValidator('enum-string.json'); 418 | $o = 'foo'; 419 | $v->validate($o); 420 | } 421 | 422 | /** 423 | * @expectedException Json\ValidationException 424 | */ 425 | public function testInvalidEnumString() 426 | { 427 | $v = $this->getValidator('enum-string.json'); 428 | $o = 'baz'; 429 | $v->validate($o); 430 | } 431 | 432 | public function testFormatDateTime() 433 | { 434 | $v = $this->getValidator('format/date-time.json'); 435 | $o = "2011-01-01T12:00:00Z"; 436 | $v->validate($o); 437 | } 438 | 439 | /** 440 | * @expectedException Json\ValidationException 441 | */ 442 | public function testInvalidFormatDateTime() 443 | { 444 | $v = $this->getValidator('format/date-time.json'); 445 | $o = "asdf"; 446 | $v->validate($o); 447 | } 448 | 449 | public function testFormatDate() 450 | { 451 | $v = $this->getValidator('format/date.json'); 452 | $o = "2011-01-01"; 453 | $v->validate($o); 454 | } 455 | 456 | /** 457 | * @expectedException Json\ValidationException 458 | */ 459 | public function testInvalidFormatDate() 460 | { 461 | $v = $this->getValidator('format/date.json'); 462 | $o = "asdf"; 463 | $v->validate($o); 464 | } 465 | 466 | public function testFormatTime() 467 | { 468 | $v = $this->getValidator('format/time.json'); 469 | $o = "12:00:00"; 470 | $v->validate($o); 471 | } 472 | 473 | /** 474 | * @expectedException Json\ValidationException 475 | */ 476 | public function testInvalidFormatTime() 477 | { 478 | $v = $this->getValidator('format/time.json'); 479 | $o = "asdf"; 480 | $v->validate($o); 481 | } 482 | 483 | public function testFormatUtcMillisec() 484 | { 485 | $v = $this->getValidator('format/utc-millisec.json'); 486 | $o = 12345; 487 | $v->validate($o); 488 | } 489 | 490 | /** 491 | * @expectedException Json\ValidationException 492 | */ 493 | public function testInvalidFormatUtcMillisec() 494 | { 495 | $v = $this->getValidator('format/utc-millisec.json'); 496 | $o = -100; 497 | $v->validate($o); 498 | } 499 | 500 | public function testFormatColor() 501 | { 502 | $v = $this->getValidator('format/color.json'); 503 | $o = "#CCC"; 504 | $v->validate($o); 505 | } 506 | 507 | /** 508 | * @expectedException Json\ValidationException 509 | */ 510 | public function testInvalidFormatColor() 511 | { 512 | $v = $this->getValidator('format/color.json'); 513 | $o = "CCC"; 514 | $v->validate($o); 515 | } 516 | 517 | public function testFormatStyle() 518 | { 519 | $v = $this->getValidator('format/style.json'); 520 | $o = "background: transparent #FFF url('/path/too/image.jpg') no-repeat 5px 10px;"; 521 | $v->validate($o); 522 | } 523 | 524 | /** 525 | * @expectedException Json\ValidationException 526 | */ 527 | public function testInvalidFormatStyle() 528 | { 529 | $v = $this->getValidator('format/style.json'); 530 | $o = "asdf"; 531 | $v->validate($o); 532 | } 533 | 534 | public function testFormatPhone() 535 | { 536 | $v = $this->getValidator('format/phone.json'); 537 | $o = "555-555-1234"; 538 | $v->validate($o); 539 | } 540 | 541 | /** 542 | * @expectedException Json\ValidationException 543 | */ 544 | public function testInvalidFormatPhone() 545 | { 546 | $v = $this->getValidator('format/phone.json'); 547 | $o = "foo"; 548 | $v->validate($o); 549 | } 550 | 551 | public function testFormatUri() 552 | { 553 | $v = $this->getValidator('format/uri.json'); 554 | $o = "http://www.example.org/page.php?a=foo&b=%20bar"; 555 | $v->validate($o); 556 | } 557 | 558 | /** 559 | * @expectedException Json\ValidationException 560 | */ 561 | public function testInvalidFormatUri() 562 | { 563 | $v = $this->getValidator('format/uri.json'); 564 | $o = "@^"; 565 | $v->validate($o); 566 | } 567 | 568 | public function testItemsSchema() 569 | { 570 | $v = $this->getValidator('items-schema.json'); 571 | $o = (object)array( 572 | 'foo' => array( 573 | (object)array( 574 | 'bar' => 'baz' 575 | ) 576 | ) 577 | ); 578 | 579 | $v->validate($o); 580 | } 581 | 582 | public function testItemsArray() 583 | { 584 | $v = $this->getValidator('items-array.json'); 585 | $o = (object)array( 586 | 'foo' => array('foo', 1) 587 | ); 588 | 589 | $v->validate($o); 590 | } 591 | 592 | /** 593 | * @expectedException Json\ValidationException 594 | */ 595 | public function testInvalidItemsArray() 596 | { 597 | $v = $this->getValidator('items-array.json'); 598 | $o = (object)array( 599 | 'foo' => array('foo', 1, true) 600 | ); 601 | 602 | $v->validate($o); 603 | } 604 | 605 | /** 606 | * @expectedException Json\SchemaException 607 | */ 608 | public function testInvalidItemsValue() 609 | { 610 | $v = $this->getValidator('invalid-items.json'); 611 | $o = (object)array( 612 | 'foo' => array('blah') 613 | ); 614 | 615 | $v->validate($o); 616 | } 617 | 618 | /** 619 | * @expectedException Json\ValidationException 620 | */ 621 | public function testInvalidItemsSchemaProperty() 622 | { 623 | $v = $this->getValidator('items-schema.json'); 624 | $o = (object)array( 625 | 'foo' => array( 626 | (object)array( 627 | 'bar' => 1 628 | ) 629 | ) 630 | ); 631 | 632 | $v->validate($o); 633 | } 634 | 635 | public function testDisallow() 636 | { 637 | $v = $this->getValidator('disallow.json'); 638 | $o = (object)array( 639 | 'foo' => 'bar' 640 | ); 641 | 642 | $v->validate($o); 643 | } 644 | 645 | /** 646 | * @expectedException Json\ValidationException 647 | */ 648 | public function testInvalidDisallow() 649 | { 650 | $v = $this->getValidator('disallow.json'); 651 | $o = (object)array( 652 | 'foo' => 123 653 | ); 654 | 655 | $v->validate($o); 656 | } 657 | 658 | public function testDivisibleBy() 659 | { 660 | $v = $this->getValidator('divisibleBy.json'); 661 | $o = 8; 662 | 663 | $v->validate($o); 664 | } 665 | 666 | /** 667 | * @expectedException Json\ValidationException 668 | */ 669 | public function testInvalidDivisibleBy() 670 | { 671 | $v = $this->getValidator('divisibleBy.json'); 672 | $o = 3; 673 | 674 | $v->validate($o); 675 | } 676 | 677 | /** 678 | * @expectedException Json\SchemaException 679 | */ 680 | public function testInvalidDivisibleByValue() 681 | { 682 | $v = $this->getValidator('invalid-divisibleBy.json'); 683 | $o = 3; 684 | 685 | $v->validate($o); 686 | } 687 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | ../tests 15 | 16 | 17 | 18 | --------------------------------------------------------------------------------