diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0b06ea4 --- /dev/null +++ b/composer.json @@ -0,0 +1,19 @@ +{ + "name": "hasbridge/json-schema-validator", + "type": "library", + "description": "PHP 5.3 implementation of json schema validation", + "keywords": ["json", "schema"], + "license": "MIT", + "authors": [{ + "name": "Harold Asbridge", + "email": "hasbridge@gmail.com" + }], + "require": { + "php": ">=5.3.2" + }, + "autoload": { + "psr-0": { + "Json": "src/" + } + } +} \ No newline at end of file diff --git a/src/Json/Validator.php b/src/Json/Validator.php index 2abf4ff..7914400 100644 --- a/src/Json/Validator.php +++ b/src/Json/Validator.php @@ -7,25 +7,25 @@ class SchemaException extends \Exception {}; /** * JSON Schema Validator - * + * * Implements schema draft version 03, as defined at http://json-schema.org - * + * * @author Harold Asbridge * @version 0.1 */ class Validator { protected $schemaDefinition; - + /** * @var stdClass */ protected $schema; - + /** * Initialize validation object - * - * @param string $schemaFile + * + * @param string $schemaFile */ public function __construct($schemaFile) { @@ -34,49 +34,119 @@ public function __construct($schemaFile) } $data = file_get_contents($schemaFile); $this->schema = json_decode($data); - + if ($this->schema === null) { throw new SchemaException('Unable to parse JSON data - syntax error?'); } - + // @TODO - validate schema itself } - + /** * Validate schema object - * + * * @param mixed $entity * @param string $entityName - * + * * @return Validator */ public function validate($entity, $entityName = null) { $entityName = $entityName ?: 'root'; - + // Validate root type $this->validateType($entity, $this->schema, $entityName); - + + return $this; + } + + /** + * Check format restriction + * + * @param mixed $entity + * @param object $schema + * @param string $entityName + * + * @return Validator + */ + public function checkFormat($entity, $schema, $entityName) + { + if (!isset($schema->format) && $schema->format) { + return $this; + } + + $valid = true; + switch ($schema->format) { + case 'date-time': + if (!preg_match('#^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$#', $entity)) { + $valid = false; + } + break; + case 'date': + if (!preg_match('#^\d{4}-\d{2}-\d{2}$#', $entity)) { + $valid = false; + } + break; + case 'time': + if (!preg_match('#^\d{2}:\d{2}:\d{2}$#', $entity)) { + $valid = false; + } + break; + case 'utc-millisec': + if ($entity < 0) { + $valid = false; + } + break; + case 'color': + if (!in_array($entity, array('maroon', 'red', 'orange', + 'yellow', 'olive', 'green', 'purple', 'fuchsia', 'lime', + 'teal', 'aqua', 'blue', 'navy', 'black', 'gray', 'silver', 'white'))) { + if (!preg_match('#^\#[0-9A-F]{6}$#', $entity) && !preg_match('#^\#[0-9A-F]{3}$#', $entity)) { + $valid = false; + } + } + break; + case 'style': + if (!preg_match('#(\.*?)[ ]?:[ ]?(.*?)#', $entity)) { + $valid = false; + } + break; + case 'phone': + if (!preg_match('#^[0-9\-+ \(\)]*$#', $entity)) { + $valid = false; + } + break; + case 'uri': + if (!preg_match('#^[A-Za-z0-9:/;,\-_\?&\.%\+\|\#=]*$#', $entity)) { + $valid = false; + } + break; + } + + if (!$valid) { + throw new ValidationException(sprintf('Value for [%s] must match format [%s]', $entityName, $schema->format)); + } + return $this; } - + /** * Validate object properties - * + * * @param object $entity * @param object $schema * @param string $entityName - * + * * @return Validator */ protected function validateProperties($entity, $schema, $entityName) { $properties = get_object_vars($entity); - + if (!isset($schema->properties)) { throw new SchemaException(sprintf('No properties defined for [%s]', $entityName)); } - + // Check defined properties foreach($schema->properties as $propertyName => $property) { if (array_key_exists($propertyName, $properties)) { @@ -90,7 +160,7 @@ protected function validateProperties($entity, $schema, $entityName) } } } - + // Check additional properties if (isset($schema->additionalProperties) && !$schema->additionalProperties) { $extra = array_diff(array_keys((array)$entity), array_keys((array)$schema->properties)); @@ -98,17 +168,17 @@ protected function validateProperties($entity, $schema, $entityName) throw new ValidationException(sprintf('Additional properties [%s] not allowed for property [%s]', implode(',', $extra), $entityName)); } } - + return $this; } /** * Validate entity type - * + * * @param mixed $entity * @param object $schema * @param string $entityName - * + * * @return Validator */ protected function validateType($entity, $schema, $entityName) @@ -116,16 +186,16 @@ protected function validateType($entity, $schema, $entityName) if (!isset($schema->type)) { throw new ValidationException(sprintf('No type given for [%s]', $entityName)); } - + $types = $schema->type; if (!is_array($types)) { $types = array($types); } - + $valid = false; - - foreach($types as $type) { - switch($type) { + + foreach ($types as $type) { + switch ($type) { case 'object': if (is_object($entity)) { $this->checkTypeObject($entity, $schema, $entityName); @@ -178,39 +248,39 @@ protected function validateType($entity, $schema, $entityName) break; } } - + if (!$valid) { throw new ValidationException(sprintf('Property [%s] must be one of the following types: [%s]', $entityName, implode(', ', $types))); } - + return $this; } - - + + /** * Check object type - * + * * @param mixed $entity * @param object $schema - * @param string $entityName - * + * @param string $entityName + * * @return Validator */ - protected function checkTypeObject($entity, $schema, $entityName) + protected function checkTypeObject($entity, $schema, $entityName) { $this->validateProperties($entity, $schema, $entityName); - + return $this; } - + /** * Check number type - * + * * @param mixed $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkTypeNumber($entity, $schema, $entityName) { @@ -222,17 +292,17 @@ protected function checkTypeNumber($entity, $schema, $entityName) $this->checkEnum($entity, $schema, $entityName); $this->checkDisallow($entity, $schema, $entityName); $this->checkDivisibleBy($entity, $schema, $entityName); - + return $this; } - + /** * Check integer type - * + * * @param mixed $entity * @param object $schema - * @param string $entityName - * + * @param string $entityName + * * @return Validator */ protected function checkTypeInteger($entity, $schema, $entityName) @@ -245,31 +315,31 @@ protected function checkTypeInteger($entity, $schema, $entityName) $this->checkEnum($entity, $schema, $entityName); $this->checkDisallow($entity, $schema, $entityName); $this->checkDivisibleBy($entity, $schema, $entityName); - + return $this; } - + /** * Check boolean type - * + * * @param mixed $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkTypeBoolean($entity, $schema, $entityName) { return $this; } - + /** * Check string type - * + * * @param mixed $entity * @param object $schema * @param string $entityName - * + * * @return Validator */ protected function checkTypeString($entity, $schema, $entityName) @@ -280,18 +350,18 @@ protected function checkTypeString($entity, $schema, $entityName) $this->checkFormat($entity, $schema, $entityName); $this->checkEnum($entity, $schema, $entityName); $this->checkDisallow($entity, $schema, $entityName); - + return $this; } - + /** * Check array type - * + * * @param mixed $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkTypeArray($entity, $schema, $entityName) { @@ -301,47 +371,47 @@ protected function checkTypeArray($entity, $schema, $entityName) $this->checkEnum($entity, $schema, $entityName); $this->checkItems($entity, $schema, $entityName); $this->checkDisallow($entity, $schema, $entityName); - + return $this; } - + /** * Check null type - * + * * @param mixed $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkTypeNull($entity, $schema, $entityName) { return $this; } - + /** * Check any type - * + * * @param mixed $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkTypeAny($entity, $schema, $entityName) { $this->checkDisallow($entity, $schema, $entityName); - + return $this; } - + /** * Check minimum value - * + * * @param int|float $entity * @param object $schema - * @param string $entityName - * + * @param string $entityName + * * @return Validator */ protected function checkMinimum($entity, $schema, $entityName) @@ -351,18 +421,18 @@ protected function checkMinimum($entity, $schema, $entityName) throw new ValidationException(sprintf('Invalid value for [%s], minimum is [%s]', $entityName, $schema->minimum)); } } - + return $this; } - + /** * Check maximum value - * + * * @param int|float $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkMaximum($entity, $schema, $entityName) { @@ -371,18 +441,18 @@ protected function checkMaximum($entity, $schema, $entityName) throw new ValidationException(sprintf('Invalid value for [%s], maximum is [%s]', $entityName, $schema->maximum)); } } - + return $this; } - + /** * Check exlusive minimum requirement - * + * * @param int|float $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkExclusiveMinimum($entity, $schema, $entityName) { @@ -391,17 +461,18 @@ protected function checkExclusiveMinimum($entity, $schema, $entityName) throw new ValidationException(sprintf('Invalid value for [%s], must be greater than [%s]', $entityName, $schema->minimum)); } } + return $this; } - + /** * Check exclusive maximum requirement - * + * * @param int|float $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkExclusiveMaximum($entity, $schema, $entityName) { @@ -410,17 +481,18 @@ protected function checkExclusiveMaximum($entity, $schema, $entityName) throw new ValidationException(sprintf('Invalid value for [%s], must be less than [%s]', $entityName, $schema->maximum)); } } + return $this; } - + /** * Check value against regex pattern - * + * * @param string $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkPattern($entity, $schema, $entityName) { @@ -429,18 +501,18 @@ protected function checkPattern($entity, $schema, $entityName) throw new ValidationException(sprintf('String does not match pattern for [%s]', $entityName)); } } - + return $this; } - + /** * Check string minimum length - * + * * @param string $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkMinLength($entity, $schema, $entityName) { @@ -449,18 +521,18 @@ protected function checkMinLength($entity, $schema, $entityName) throw new ValidationException(sprintf('String too short for [%s], minimum length is [%s]', $entityName, $schema->minLength)); } } - + return $this; } - + /** * Check string maximum length - * + * * @param string $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkMaxLength($entity, $schema, $entityName) { @@ -469,18 +541,18 @@ protected function checkMaxLength($entity, $schema, $entityName) throw new ValidationException(sprintf('String too long for [%s], maximum length is [%s]', $entityName, $schema->maxLength)); } } - + return $this; } - + /** * Check array minimum items - * + * * @param array $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkMinItems($entity, $schema, $entityName) { @@ -489,18 +561,18 @@ protected function checkMinItems($entity, $schema, $entityName) throw new ValidationException(sprintf('Not enough array items for [%s], minimum is [%s]', $entityName, $schema->minItems)); } } - + return $this; } - + /** * Check array maximum items - * + * * @param array $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkMaxItems($entity, $schema, $entityName) { @@ -509,18 +581,18 @@ protected function checkMaxItems($entity, $schema, $entityName) throw new ValidationException(sprintf('Too many array items for [%s], maximum is [%s]', $entityName, $schema->maxItems)); } } - + return $this; } - + /** * Check array unique items - * + * * @param array $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkUniqueItems($entity, $schema, $entityName) { @@ -529,18 +601,18 @@ protected function checkUniqueItems($entity, $schema, $entityName) throw new ValidationException(sprintf('All items in array [%s] must be unique', $entityName)); } } - + return $this; } - + /** * Check enum restriction - * + * * @param array $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkEnum($entity, $schema, $entityName) { @@ -550,7 +622,7 @@ protected function checkEnum($entity, $schema, $entityName) throw new SchemaException(sprintf('Enum property must be an array for [%s]', $entityName)); } if (is_array($entity)) { - foreach($entity as $val) { + foreach ($entity as $val) { if (!in_array($val, $schema->enum)) { $valid = false; } @@ -561,89 +633,22 @@ protected function checkEnum($entity, $schema, $entityName) } } } - + if (!$valid) { throw new ValidationException(sprintf('Invalid value(s) for [%s], allowable values are [%s]', $entityName, implode(',', $schema->enum))); } - - return $this; - } - - /** - * Check format restriction - * - * @param mixed $entity - * @param object $schema - * @param string $entityName - * - * @return Validator - */ - public function checkFormat($entity, $schema, $entityName) - { - if (isset($schema->format) && $schema->format) { - $valid = true; - switch($schema->format) { - case 'date-time': - if (!preg_match('#^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$#', $entity)) { - $valid = false; - } - break; - case 'date': - if (!preg_match('#^\d{4}-\d{2}-\d{2}$#', $entity)) { - $valid = false; - } - break; - case 'time': - if (!preg_match('#^\d{2}:\d{2}:\d{2}$#', $entity)) { - $valid = false; - } - break; - case 'utc-millisec': - if ($entity < 0) { - $valid = false; - } - break; - case 'color': - if (!in_array($entity, array('maroon', 'red', 'orange', - 'yellow', 'olive', 'green', 'purple', 'fuchsia', 'lime', - 'teal', 'aqua', 'blue', 'navy', 'black', 'gray', 'silver', 'white'))) { - if (!preg_match('#^\#[0-9A-F]{6}$#', $entity) && !preg_match('#^\#[0-9A-F]{3}$#', $entity)) { - $valid = false; - } - } - break; - case 'style': - if (!preg_match('#(\.*?)[ ]?:[ ]?(.*?)#', $entity)) { - $valid = false; - } - break; - case 'phone': - if (!preg_match('#^[0-9\-+ \(\)]*$#', $entity)) { - $valid = false; - } - break; - case 'uri': - if (!preg_match('#^[A-Za-z0-9:/;,\-_\?&\.%\+\|\#=]*$#', $entity)) { - $valid = false; - } - break; - } - - if (!$valid) { - throw new ValidationException(sprintf('Value for [%s] must match format [%s]', $entityName, $schema->format)); - } - } + return $this; } - + /** * Check items restriction - * + * * @param array $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkItems($entity, $schema, $entityName) { @@ -652,7 +657,7 @@ protected function checkItems($entity, $schema, $entityName) if (is_array($schema->items)) { foreach($entity as $index => $node) { $nodeEntityName = $entityName . '[' . $index . ']'; - + // Check if the item passes any of the item validations foreach($schema->items as $item) { $nodeValid = true; @@ -664,13 +669,13 @@ protected function checkItems($entity, $schema, $entityName) $nodeValid = false; } } - + // If item did not pass any item validations if (!$nodeValid) { $allowedTypes = array_map(function($item){ return $item->type == 'object' ? 'object (schema)' : $item->type; }, $schema->items); - throw new ValidationException(sprintf('Invalid value for [%s], must be one of the following types: [%s]', + throw new ValidationException(sprintf('Invalid value for [%s], must be one of the following types: [%s]', $nodeEntityName, implode(', ' , $allowedTypes))); } } @@ -680,22 +685,22 @@ protected function checkItems($entity, $schema, $entityName) $nodeEntityName = $entityName . '[' . $index . ']'; $this->validateType($node, $schema->items, $nodeEntityName); } - + } else { throw new SchemaException(sprintf('Invalid items value for [%s]', $entityName)); } } - + return $this; } - + /** * Check disallowed entity type - * + * * @param mixed $entity * @param object $schema - * @param string $entityName - * + * @param string $entityName + * * @return Validator */ protected function checkDisallow($entity, $schema, $entityName) @@ -704,7 +709,7 @@ protected function checkDisallow($entity, $schema, $entityName) $thisSchema = clone $schema; $thisSchema->type = $schema->disallow; unset($thisSchema->disallow); - + // We are expecting an exception - if one is not thrown, // then we have a matching disallowed type try { @@ -717,22 +722,22 @@ protected function checkDisallow($entity, $schema, $entityName) $disallowedTypes = array_map(function($item){ return is_object($item) ? 'object (schema)' : $item; }, is_array($schema->disallow) ? $schema->disallow : array($schema->disallow)); - throw new ValidationException(sprintf('Invalid value for [%s], disallowed types are [%s]', + throw new ValidationException(sprintf('Invalid value for [%s], disallowed types are [%s]', $entityName, implode(', ', $disallowedTypes))); } } - + return $this; } - + /** * Check divisibleby restriction - * + * * @param int|float $entity * @param object $schema * @param string $entityName - * - * @return Validator + * + * @return Validator */ protected function checkDivisibleBy($entity, $schema, $entityName) { @@ -740,12 +745,12 @@ protected function checkDivisibleBy($entity, $schema, $entityName) if (!is_numeric($schema->divisibleBy)) { throw new SchemaException(sprintf('Invalid divisibleBy value for [%s], must be numeric', $entityName)); } - + if ($entity % $schema->divisibleBy != 0) { throw new ValidationException(sprintf('Invalid value for [%s], must be divisible by [%d]', $entityName, $schema->divisibleBy)); } } - + return $this; } } \ No newline at end of file