diff --git a/README.md b/README.md index 376c2e27..8df14db7 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,10 @@ third argument to `Validator::validate()`, or can be provided as the third argum | `Constraint::CHECK_MODE_TYPE_CAST` | Enable fuzzy type checking for associative arrays and objects | | `Constraint::CHECK_MODE_COERCE_TYPES` | Convert data types to match the schema where possible | | `Constraint::CHECK_MODE_APPLY_DEFAULTS` | Apply default values from the schema if not set | +| `Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS` | When applying defaults, only set values that are required | | `Constraint::CHECK_MODE_EXCEPTIONS` | Throw an exception immediately if validation fails | +| `Constraint::CHECK_MODE_DISABLE_FORMAT` | Do not validate "format" constraints | +| `Constraint::CHECK_MODE_VALIDATE_SCHEMA` | Validate the schema as well as the provided document | Please note that using `Constraint::CHECK_MODE_COERCE_TYPES` or `Constraint::CHECK_MODE_APPLY_DEFAULTS` will modify your original data. diff --git a/bin/validate-json b/bin/validate-json index e9c18095..421ebcde 100755 --- a/bin/validate-json +++ b/bin/validate-json @@ -17,7 +17,6 @@ function __autoload($className) { $className = ltrim($className, '\\'); $fileName = ''; - $namespace = ''; if ($lastNsPos = strrpos($className, '\\')) { $namespace = substr($className, 0, $lastNsPos); $className = substr($className, $lastNsPos + 1); @@ -29,6 +28,49 @@ function __autoload($className) } } +// support running this tool from git checkout +if (is_dir(__DIR__ . '/../src/JsonSchema')) { + set_include_path(__DIR__ . '/../src' . PATH_SEPARATOR . get_include_path()); +} + +$arOptions = array(); +$arArgs = array(); +array_shift($argv);//script itself +foreach ($argv as $arg) { + if ($arg{0} == '-') { + $arOptions[$arg] = true; + } else { + $arArgs[] = $arg; + } +} + +if (count($arArgs) == 0 + || isset($arOptions['--help']) || isset($arOptions['-h']) +) { + echo <<getMessage() . "\n"; + output("Error loading JSON schema file\n"); + output($urlSchema . "\n"); + output($e->getMessage() . "\n"); exit(2); } $refResolver = new JsonSchema\SchemaStorage($retriever, $resolver); @@ -221,17 +233,19 @@ try { $validator->check($data, $schema); if ($validator->isValid()) { - echo "OK. The supplied JSON validates against the schema.\n"; + if(isset($arOptions['--verbose'])) { + output("OK. The supplied JSON validates against the schema.\n"); + } } else { - echo "JSON does not validate. Violations:\n"; + output("JSON does not validate. Violations:\n"); foreach ($validator->getErrors() as $error) { - echo sprintf("[%s] %s\n", $error['property'], $error['message']); + output(sprintf("[%s] %s\n", $error['property'], $error['message'])); } exit(23); } } catch (Exception $e) { - echo "JSON does not validate. Error:\n"; - echo $e->getMessage() . "\n"; - echo "Error code: " . $e->getCode() . "\n"; + output("JSON does not validate. Error:\n"); + output($e->getMessage() . "\n"); + output("Error code: " . $e->getCode() . "\n"); exit(24); } diff --git a/dist/schema/json-schema-draft-03.json b/dist/schema/json-schema-draft-03.json new file mode 100644 index 00000000..7a1a2d38 --- /dev/null +++ b/dist/schema/json-schema-draft-03.json @@ -0,0 +1,174 @@ +{ + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "http://json-schema.org/draft-03/schema#", + "type": "object", + + "properties": { + "type": { + "type": [ "string", "array" ], + "items": { + "type": [ "string", { "$ref": "#" } ] + }, + "uniqueItems": true, + "default": "any" + }, + + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + + "additionalProperties": { + "type": [ { "$ref": "#" }, "boolean" ], + "default": {} + }, + + "items": { + "type": [ { "$ref": "#" }, "array" ], + "items": { "$ref": "#" }, + "default": {} + }, + + "additionalItems": { + "type": [ { "$ref": "#" }, "boolean" ], + "default": {} + }, + + "required": { + "type": "boolean", + "default": false + }, + + "dependencies": { + "type": "object", + "additionalProperties": { + "type": [ "string", "array", { "$ref": "#" } ], + "items": { + "type": "string" + } + }, + "default": {} + }, + + "minimum": { + "type": "number" + }, + + "maximum": { + "type": "number" + }, + + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + + "minItems": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + + "maxItems": { + "type": "integer", + "minimum": 0 + }, + + "uniqueItems": { + "type": "boolean", + "default": false + }, + + "pattern": { + "type": "string", + "format": "regex" + }, + + "minLength": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + + "maxLength": { + "type": "integer" + }, + + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + + "default": { + "type": "any" + }, + + "title": { + "type": "string" + }, + + "description": { + "type": "string" + }, + + "format": { + "type": "string" + }, + + "divisibleBy": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true, + "default": 1 + }, + + "disallow": { + "type": [ "string", "array" ], + "items": { + "type": [ "string", { "$ref": "#" } ] + }, + "uniqueItems": true + }, + + "extends": { + "type": [ { "$ref": "#" }, "array" ], + "items": { "$ref": "#" }, + "default": {} + }, + + "id": { + "type": "string", + "format": "uri" + }, + + "$ref": { + "type": "string", + "format": "uri" + }, + + "$schema": { + "type": "string", + "format": "uri" + } + }, + + "dependencies": { + "exclusiveMinimum": "minimum", + "exclusiveMaximum": "maximum" + }, + + "default": {} +} diff --git a/dist/schema/json-schema-draft-04.json b/dist/schema/json-schema-draft-04.json new file mode 100644 index 00000000..85eb502a --- /dev/null +++ b/dist/schema/json-schema-draft-04.json @@ -0,0 +1,150 @@ +{ + "id": "http://json-schema.org/draft-04/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "positiveInteger": { + "type": "integer", + "minimum": 0 + }, + "positiveIntegerDefault0": { + "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] + }, + "simpleTypes": { + "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "uniqueItems": true + } + }, + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uri" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": {}, + "multipleOf": { + "type": "number", + "minimum": 0, + "exclusiveMinimum": true + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "boolean", + "default": false + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "boolean", + "default": false + }, + "maxLength": { "$ref": "#/definitions/positiveInteger" }, + "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": {} + }, + "maxItems": { "$ref": "#/definitions/positiveInteger" }, + "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxProperties": { "$ref": "#/definitions/positiveInteger" }, + "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { + "anyOf": [ + { "type": "boolean" }, + { "$ref": "#" } + ], + "default": {} + }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "dependencies": { + "exclusiveMaximum": [ "maximum" ], + "exclusiveMinimum": [ "minimum" ] + }, + "default": {} +} diff --git a/src/JsonSchema/Constraints/BaseConstraint.php b/src/JsonSchema/Constraints/BaseConstraint.php index ef1bdc54..eefb08ed 100644 --- a/src/JsonSchema/Constraints/BaseConstraint.php +++ b/src/JsonSchema/Constraints/BaseConstraint.php @@ -10,7 +10,9 @@ namespace JsonSchema\Constraints; use JsonSchema\Entity\JsonPointer; +use JsonSchema\Exception\InvalidArgumentException; use JsonSchema\Exception\ValidationException; +use JsonSchema\Validator; /** * A more basic constraint definition - used for the public @@ -23,6 +25,11 @@ class BaseConstraint */ protected $errors = array(); + /** + * @var int All error types which have occurred + */ + protected $errorMask = Validator::ERROR_NONE; + /** * @var Factory */ @@ -43,6 +50,7 @@ public function addError(JsonPointer $path = null, $message, $constraint = '', a 'pointer' => ltrim(strval($path ?: new JsonPointer('')), '#'), 'message' => $message, 'constraint' => $constraint, + 'context' => $this->factory->getErrorContext(), ); if ($this->factory->getConfig(Constraint::CHECK_MODE_EXCEPTIONS)) { @@ -54,18 +62,42 @@ public function addError(JsonPointer $path = null, $message, $constraint = '', a } $this->errors[] = $error; + $this->errorMask |= $error['context']; } public function addErrors(array $errors) { if ($errors) { $this->errors = array_merge($this->errors, $errors); + $errorMask = &$this->errorMask; + array_walk($errors, function ($error) use (&$errorMask) { + if (isset($error['context'])) { + $errorMask |= $error['context']; + } + }); } } - public function getErrors() + public function getErrors($errorContext = Validator::ERROR_ALL) { - return $this->errors; + if ($errorContext === Validator::ERROR_ALL) { + return $this->errors; + } + + return array_filter($this->errors, function ($error) use ($errorContext) { + if ($errorContext & $error['context']) { + return true; + } + }); + } + + public function numErrors($errorContext = Validator::ERROR_ALL) + { + if ($errorContext === Validator::ERROR_ALL) { + return count($this->errors); + } + + return count($this->getErrors($errorContext)); } public function isValid() @@ -80,5 +112,37 @@ public function isValid() public function reset() { $this->errors = array(); + $this->errorMask = Validator::ERROR_NONE; + } + + /** + * Get the error mask + * + * @return int + */ + public function getErrorMask() + { + return $this->errorMask; + } + + /** + * Recursively cast an associative array to an object + * + * @param array $array + * + * @return object + */ + public static function arrayToObjectRecursive($array) + { + $json = json_encode($array); + if (json_last_error() !== \JSON_ERROR_NONE) { + $message = 'Unable to encode schema array as JSON'; + if (version_compare(phpversion(), '5.5.0', '>=')) { + $message .= ': ' . json_last_error_msg(); + } + throw new InvalidArgumentException($message); + } + + return json_decode($json); } } diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index 7fa0a99a..b7f3bb42 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -30,6 +30,9 @@ abstract class Constraint extends BaseConstraint implements ConstraintInterface const CHECK_MODE_COERCE_TYPES = 0x00000004; const CHECK_MODE_APPLY_DEFAULTS = 0x00000008; const CHECK_MODE_EXCEPTIONS = 0x00000010; + const CHECK_MODE_DISABLE_FORMAT = 0x00000020; + const CHECK_MODE_ONLY_REQUIRED_DEFAULTS = 0x00000080; + const CHECK_MODE_VALIDATE_SCHEMA = 0x00000100; /** * Bubble down the path @@ -77,10 +80,10 @@ protected function checkArray(&$value, $schema = null, JsonPointer $path = null, * @param mixed $i * @param mixed $patternProperties */ - protected function checkObject(&$value, $schema = null, JsonPointer $path = null, $i = null, $patternProperties = null) + protected function checkObject(&$value, $schema = null, JsonPointer $path = null, $i = null, $patternProperties = null, $appliedDefaults = array()) { $validator = $this->factory->createInstanceFor('object'); - $validator->check($value, $schema, $path, $i, $patternProperties); + $validator->check($value, $schema, $path, $i, $patternProperties, $appliedDefaults); $this->addErrors($validator->getErrors()); } @@ -109,11 +112,11 @@ protected function checkType(&$value, $schema = null, JsonPointer $path = null, * @param JsonPointer|null $path * @param mixed $i */ - protected function checkUndefined(&$value, $schema = null, JsonPointer $path = null, $i = null) + protected function checkUndefined(&$value, $schema = null, JsonPointer $path = null, $i = null, $fromDefault = false) { $validator = $this->factory->createInstanceFor('undefined'); - $validator->check($value, $this->factory->getSchemaStorage()->resolveRefSchema($schema), $path, $i); + $validator->check($value, $this->factory->getSchemaStorage()->resolveRefSchema($schema), $path, $i, $fromDefault); $this->addErrors($validator->getErrors()); } diff --git a/src/JsonSchema/Constraints/Factory.php b/src/JsonSchema/Constraints/Factory.php index 8c24873f..c9296385 100644 --- a/src/JsonSchema/Constraints/Factory.php +++ b/src/JsonSchema/Constraints/Factory.php @@ -16,6 +16,7 @@ use JsonSchema\SchemaStorageInterface; use JsonSchema\Uri\UriRetriever; use JsonSchema\UriRetrieverInterface; +use JsonSchema\Validator; /** * Factory for centralize constraint initialization. @@ -42,6 +43,11 @@ class Factory */ private $typeCheck = array(); + /** + * @var int Validation context + */ + protected $errorContext = Validator::ERROR_DOCUMENT_VALIDATION; + /** * @var array */ @@ -193,4 +199,24 @@ public function createInstanceFor($constraintName) return clone $this->instanceCache[$constraintName]; } + + /** + * Get the error context + * + * @return string + */ + public function getErrorContext() + { + return $this->errorContext; + } + + /** + * Set the error context + * + * @param string $validationContext + */ + public function setErrorContext($errorContext) + { + $this->errorContext = $errorContext; + } } diff --git a/src/JsonSchema/Constraints/FormatConstraint.php b/src/JsonSchema/Constraints/FormatConstraint.php index ad192b5b..c172847f 100644 --- a/src/JsonSchema/Constraints/FormatConstraint.php +++ b/src/JsonSchema/Constraints/FormatConstraint.php @@ -26,7 +26,7 @@ class FormatConstraint extends Constraint */ public function check(&$element, $schema = null, JsonPointer $path = null, $i = null) { - if (!isset($schema->format)) { + if (!isset($schema->format) || $this->factory->getConfig(self::CHECK_MODE_DISABLE_FORMAT)) { return; } @@ -105,7 +105,12 @@ public function check(&$element, $schema = null, JsonPointer $path = null, $i = break; case 'email': - if (null === filter_var($element, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE)) { + $filterFlags = FILTER_NULL_ON_FAILURE; + if (defined('FILTER_FLAG_EMAIL_UNICODE')) { + // Only available from PHP >= 7.1.0, so ignore it for coverage checks + $filterFlags |= constant('FILTER_FLAG_EMAIL_UNICODE'); // @codeCoverageIgnore + } + if (null === filter_var($element, FILTER_VALIDATE_EMAIL, $filterFlags)) { $this->addError($path, 'Invalid email', 'format', array('format' => $schema->format)); } break; diff --git a/src/JsonSchema/Constraints/ObjectConstraint.php b/src/JsonSchema/Constraints/ObjectConstraint.php index 5ea94f7d..0b9a7da3 100644 --- a/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/src/JsonSchema/Constraints/ObjectConstraint.php @@ -19,15 +19,22 @@ */ class ObjectConstraint extends Constraint { + /** + * @var array List of properties to which a default value has been applied + */ + protected $appliedDefaults = array(); + /** * {@inheritdoc} */ - public function check(&$element, $definition = null, JsonPointer $path = null, $additionalProp = null, $patternProperties = null) + public function check(&$element, $definition = null, JsonPointer $path = null, $additionalProp = null, $patternProperties = null, $appliedDefaults = array()) { if ($element instanceof UndefinedConstraint) { return; } + $this->appliedDefaults = $appliedDefaults; + $matches = array(); if ($patternProperties) { $matches = $this->validatePatternProperties($element, $path, $patternProperties); @@ -63,7 +70,7 @@ public function validatePatternProperties($element, JsonPointer $path = null, $p foreach ($element as $i => $value) { if (preg_match($delimiter . $pregex . $delimiter . 'u', $i)) { $matches[] = $i; - $this->checkUndefined($value, $schema ?: new \stdClass(), $path, $i); + $this->checkUndefined($value, $schema ?: new \stdClass(), $path, $i, in_array($i, $this->appliedDefaults)); } } } @@ -95,9 +102,9 @@ public function validateElement($element, $matches, $objectDefinition = null, Js // additional properties defined if (!in_array($i, $matches) && $additionalProp && !$definition) { if ($additionalProp === true) { - $this->checkUndefined($value, null, $path, $i); + $this->checkUndefined($value, null, $path, $i, in_array($i, $this->appliedDefaults)); } else { - $this->checkUndefined($value, $additionalProp, $path, $i); + $this->checkUndefined($value, $additionalProp, $path, $i, in_array($i, $this->appliedDefaults)); } } @@ -131,7 +138,7 @@ public function validateDefinition(&$element, $objectDefinition = null, JsonPoin if (is_object($definition)) { // Undefined constraint will check for is_object() and quit if is not - so why pass it? - $this->checkUndefined($property, $definition, $path, $i); + $this->checkUndefined($property, $definition, $path, $i, in_array($i, $this->appliedDefaults)); } } } diff --git a/src/JsonSchema/Constraints/SchemaConstraint.php b/src/JsonSchema/Constraints/SchemaConstraint.php index c33fe8ca..6b4fefde 100644 --- a/src/JsonSchema/Constraints/SchemaConstraint.php +++ b/src/JsonSchema/Constraints/SchemaConstraint.php @@ -11,6 +11,10 @@ use JsonSchema\Entity\JsonPointer; use JsonSchema\Exception\InvalidArgumentException; +use JsonSchema\Exception\InvalidSchemaException; +use JsonSchema\Exception\RuntimeException; +use JsonSchema\SchemaStorage; +use JsonSchema\Validator; /** * The SchemaConstraint Constraints, validates an element against a given schema @@ -20,6 +24,8 @@ */ class SchemaConstraint extends Constraint { + const DEFAULT_SCHEMA_SPEC = 'http://json-schema.org/draft-04/schema#'; + /** * {@inheritdoc} */ @@ -27,16 +33,62 @@ public function check(&$element, $schema = null, JsonPointer $path = null, $i = { if ($schema !== null) { // passed schema - $this->checkUndefined($element, $schema, $path, $i); + $validationSchema = $schema; } elseif ($this->getTypeCheck()->propertyExists($element, $this->inlineSchemaProperty)) { $inlineSchema = $this->getTypeCheck()->propertyGet($element, $this->inlineSchemaProperty); if (is_array($inlineSchema)) { $inlineSchema = json_decode(json_encode($inlineSchema)); } // inline schema - $this->checkUndefined($element, $inlineSchema, $path, $i); + $validationSchema = $inlineSchema; } else { throw new InvalidArgumentException('no schema found to verify against'); } + + // validate schema against whatever is defined in $validationSchema->$schema. If no + // schema is defined, assume self::DEFAULT_SCHEMA_SPEC (currently draft-04). + if ($this->factory->getConfig(self::CHECK_MODE_VALIDATE_SCHEMA)) { + if (!$this->getTypeCheck()->isObject($validationSchema)) { + throw new RuntimeException('Cannot validate the schema of a non-object'); + } + if ($this->getTypeCheck()->propertyExists($validationSchema, '$schema')) { + $schemaSpec = $this->getTypeCheck()->propertyGet($validationSchema, '$schema'); + } else { + $schemaSpec = self::DEFAULT_SCHEMA_SPEC; + } + + // get the spec schema + $schemaStorage = $this->factory->getSchemaStorage(); + if (!$this->getTypeCheck()->isObject($schemaSpec)) { + $schemaSpec = $schemaStorage->getSchema($schemaSpec); + } + + // save error count, config & subtract CHECK_MODE_VALIDATE_SCHEMA + $initialErrorCount = $this->numErrors(); + $initialConfig = $this->factory->getConfig(); + $initialContext = $this->factory->getErrorContext(); + $this->factory->removeConfig(self::CHECK_MODE_VALIDATE_SCHEMA | self::CHECK_MODE_APPLY_DEFAULTS); + $this->factory->addConfig(self::CHECK_MODE_TYPE_CAST); + $this->factory->setErrorContext(Validator::ERROR_SCHEMA_VALIDATION); + + // validate schema + try { + $this->check($validationSchema, $schemaSpec); + } catch (\Exception $e) { + if ($this->factory->getConfig(self::CHECK_MODE_EXCEPTIONS)) { + throw new InvalidSchemaException('Schema did not pass validation', 0, $e); + } + } + if ($this->numErrors() > $initialErrorCount) { + $this->addError($path, 'Schema is not valid', 'schema'); + } + + // restore the initial config + $this->factory->setConfig($initialConfig); + $this->factory->setErrorContext($initialContext); + } + + // validate element against $validationSchema + $this->checkUndefined($element, $validationSchema, $path, $i); } } diff --git a/src/JsonSchema/Constraints/StringConstraint.php b/src/JsonSchema/Constraints/StringConstraint.php index 5b15de7a..c66af1ed 100644 --- a/src/JsonSchema/Constraints/StringConstraint.php +++ b/src/JsonSchema/Constraints/StringConstraint.php @@ -54,6 +54,7 @@ private function strlen($string) return mb_strlen($string, mb_detect_encoding($string)); } - return strlen($string); + // mbstring is present on all test platforms, so strlen() can be ignored for coverage + return strlen($string); // @codeCoverageIgnore } } diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 147e5bc3..ec43467e 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -11,6 +11,7 @@ use JsonSchema\Constraints\TypeCheck\LooseTypeCheck; use JsonSchema\Entity\JsonPointer; +use JsonSchema\Exception\ValidationException; use JsonSchema\Uri\UriResolver; /** @@ -21,16 +22,24 @@ */ class UndefinedConstraint extends Constraint { + /** + * @var array List of properties to which a default value has been applied + */ + protected $appliedDefaults = array(); + /** * {@inheritdoc} */ - public function check(&$value, $schema = null, JsonPointer $path = null, $i = null) + public function check(&$value, $schema = null, JsonPointer $path = null, $i = null, $fromDefault = false) { if (is_null($schema) || !is_object($schema)) { return; } $path = $this->incrementPath($path ?: new JsonPointer(''), $i); + if ($fromDefault) { + $path->setFromDefault(); + } // check special properties $this->validateCommonProperties($value, $schema, $path, $i); @@ -66,7 +75,8 @@ public function validateTypes(&$value, $schema = null, JsonPointer $path, $i = n isset($schema->properties) ? $this->factory->getSchemaStorage()->resolveRefSchema($schema->properties) : $schema, $path, isset($schema->additionalProperties) ? $schema->additionalProperties : null, - isset($schema->patternProperties) ? $schema->patternProperties : null + isset($schema->patternProperties) ? $schema->patternProperties : null, + $this->appliedDefaults ); } @@ -111,46 +121,8 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer } // Apply default values from schema - if ($this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) { - if ($this->getTypeCheck()->isObject($value) && isset($schema->properties)) { - // $value is an object, so apply default properties if defined - foreach ($schema->properties as $currentProperty => $propertyDefinition) { - if (!$this->getTypeCheck()->propertyExists($value, $currentProperty) && isset($propertyDefinition->default)) { - if (is_object($propertyDefinition->default)) { - $this->getTypeCheck()->propertySet($value, $currentProperty, clone $propertyDefinition->default); - } else { - $this->getTypeCheck()->propertySet($value, $currentProperty, $propertyDefinition->default); - } - } - } - } elseif ($this->getTypeCheck()->isArray($value)) { - if (isset($schema->properties)) { - // $value is an array, but default properties are defined, so treat as assoc - foreach ($schema->properties as $currentProperty => $propertyDefinition) { - if (!isset($value[$currentProperty]) && isset($propertyDefinition->default)) { - if (is_object($propertyDefinition->default)) { - $value[$currentProperty] = clone $propertyDefinition->default; - } else { - $value[$currentProperty] = $propertyDefinition->default; - } - } - } - } elseif (isset($schema->items)) { - // $value is an array, and default items are defined - treat as plain array - foreach ($schema->items as $currentProperty => $itemDefinition) { - if (!isset($value[$currentProperty]) && isset($itemDefinition->default)) { - if (is_object($itemDefinition->default)) { - $value[$currentProperty] = clone $itemDefinition->default; - } else { - $value[$currentProperty] = $itemDefinition->default; - } - } - } - } - } elseif (($value instanceof self || $value === null) && isset($schema->default)) { - // $value is a leaf, not a container - apply the default directly - $value = is_object($schema->default) ? clone $schema->default : $schema->default; - } + if (!$path->fromDefault()) { + $this->applyDefaultValues($value, $schema, $path); } // Verify required values @@ -213,6 +185,96 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer } } + /** + * Check whether a default should be applied for this value + * + * @param mixed $schema + * @param mixed $parentSchema + * @param bool $requiredOnly + * + * @return bool + */ + private function shouldApplyDefaultValue($requiredOnly, $schema, $name = null, $parentSchema = null) + { + // required-only mode is off + if (!$requiredOnly) { + return true; + } + // draft-04 required is set + if ( + $name !== null + && isset($parentSchema->required) + && is_array($parentSchema->required) + && in_array($name, $parentSchema->required) + ) { + return true; + } + // draft-03 required is set + if (isset($schema->required) && !is_array($schema->required) && $schema->required) { + return true; + } + // default case + return false; + } + + /** + * Apply default values + * + * @param mixed $value + * @param mixed $schema + * @param JsonPointer $path + */ + protected function applyDefaultValues(&$value, $schema, $path) + { + // only apply defaults if feature is enabled + if (!$this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) { + return; + } + + // apply defaults if appropriate + $requiredOnly = $this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_DEFAULTS); + if (isset($schema->properties) && LooseTypeCheck::isObject($value)) { + // $value is an object or assoc array, and properties are defined - treat as an object + foreach ($schema->properties as $currentProperty => $propertyDefinition) { + if ( + !LooseTypeCheck::propertyExists($value, $currentProperty) + && property_exists($propertyDefinition, 'default') + && $this->shouldApplyDefaultValue($requiredOnly, $propertyDefinition, $currentProperty, $schema) + ) { + // assign default value + if (is_object($propertyDefinition->default)) { + LooseTypeCheck::propertySet($value, $currentProperty, clone $propertyDefinition->default); + } else { + LooseTypeCheck::propertySet($value, $currentProperty, $propertyDefinition->default); + } + $this->appliedDefaults[] = $currentProperty; + } + } + } elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) { + // $value is an array, and items are defined - treat as plain array + foreach ($schema->items as $currentItem => $itemDefinition) { + if ( + !array_key_exists($currentItem, $value) + && property_exists($itemDefinition, 'default') + && $this->shouldApplyDefaultValue($requiredOnly, $itemDefinition)) { + if (is_object($itemDefinition->default)) { + $value[$currentItem] = clone $itemDefinition->default; + } else { + $value[$currentItem] = $itemDefinition->default; + } + } + $path->setFromDefault(); + } + } elseif ( + $value instanceof self + && property_exists($schema, 'default') + && $this->shouldApplyDefaultValue($requiredOnly, $schema)) { + // $value is a leaf, not a container - apply the default directly + $value = is_object($schema->default) ? clone $schema->default : $schema->default; + $path->setFromDefault(); + } + } + /** * Validate allOf, anyOf, and oneOf properties * @@ -243,11 +305,16 @@ protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i if (isset($schema->anyOf)) { $isValid = false; $startErrors = $this->getErrors(); + $caughtException = null; foreach ($schema->anyOf as $anyOf) { $initErrors = $this->getErrors(); - $this->checkUndefined($value, $anyOf, $path, $i); - if ($isValid = (count($this->getErrors()) == count($initErrors))) { - break; + try { + $this->checkUndefined($value, $anyOf, $path, $i); + if ($isValid = (count($this->getErrors()) == count($initErrors))) { + break; + } + } catch (ValidationException $e) { + $isValid = false; } } if (!$isValid) { @@ -262,12 +329,17 @@ protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i $matchedSchemas = 0; $startErrors = $this->getErrors(); foreach ($schema->oneOf as $oneOf) { - $this->errors = array(); - $this->checkUndefined($value, $oneOf, $path, $i); - if (count($this->getErrors()) == 0) { - $matchedSchemas++; + try { + $this->errors = array(); + $this->checkUndefined($value, $oneOf, $path, $i); + if (count($this->getErrors()) == 0) { + $matchedSchemas++; + } + $allErrors = array_merge($allErrors, array_values($this->getErrors())); + } catch (ValidationException $e) { + // deliberately do nothing here - validation failed, but we want to check + // other schema options in the OneOf field. } - $allErrors = array_merge($allErrors, array_values($this->getErrors())); } if ($matchedSchemas !== 1) { $this->addErrors(array_merge($allErrors, $startErrors)); diff --git a/src/JsonSchema/Entity/JsonPointer.php b/src/JsonSchema/Entity/JsonPointer.php index 31c753ba..fcaf5b8d 100644 --- a/src/JsonSchema/Entity/JsonPointer.php +++ b/src/JsonSchema/Entity/JsonPointer.php @@ -9,6 +9,8 @@ namespace JsonSchema\Entity; +use JsonSchema\Exception\InvalidArgumentException; + /** * @package JsonSchema\Entity * @@ -22,15 +24,20 @@ class JsonPointer /** @var string[] */ private $propertyPaths = array(); + /** + * @var bool Whether the value at this path was set from a schema default + */ + private $fromDefault = false; + /** * @param string $value * - * @throws \InvalidArgumentException when $value is not a string + * @throws InvalidArgumentException when $value is not a string */ public function __construct($value) { if (!is_string($value)) { - throw new \InvalidArgumentException('Ref value must be a string'); + throw new InvalidArgumentException('Ref value must be a string'); } $splitRef = explode('#', $value, 2); @@ -133,4 +140,22 @@ public function __toString() { return $this->getFilename() . $this->getPropertyPathAsString(); } + + /** + * Mark the value at this path as being set from a schema default + */ + public function setFromDefault() + { + $this->fromDefault = true; + } + + /** + * Check whether the value at this path was set from a schema default + * + * @return bool + */ + public function fromDefault() + { + return $this->fromDefault; + } } diff --git a/src/JsonSchema/Exception/InvalidSchemaException.php b/src/JsonSchema/Exception/InvalidSchemaException.php new file mode 100644 index 00000000..c1209958 --- /dev/null +++ b/src/JsonSchema/Exception/InvalidSchemaException.php @@ -0,0 +1,17 @@ +uriRetriever->retrieve($id); } $objectIterator = new ObjectIterator($schema); @@ -74,8 +79,18 @@ public function getSchema($id) public function resolveRef($ref) { $jsonPointer = new JsonPointer($ref); - $refSchema = $this->getSchema($jsonPointer->getFilename()); + // resolve filename for pointer + $fileName = $jsonPointer->getFilename(); + if (!strlen($fileName)) { + throw new UnresolvableJsonPointerException(sprintf( + "Could not resolve fragment '%s': no file is defined", + $jsonPointer->getPropertyPathAsString() + )); + } + + // get & process the schema + $refSchema = $this->getSchema($fileName); foreach ($jsonPointer->getPropertyPaths() as $path) { if (is_object($refSchema) && property_exists($refSchema, $path)) { $refSchema = $this->resolveRefSchema($refSchema->{$path}); diff --git a/src/JsonSchema/Uri/Retrievers/Curl.php b/src/JsonSchema/Uri/Retrievers/Curl.php index a4125aa6..81c86037 100644 --- a/src/JsonSchema/Uri/Retrievers/Curl.php +++ b/src/JsonSchema/Uri/Retrievers/Curl.php @@ -9,6 +9,7 @@ namespace JsonSchema\Uri\Retrievers; +use JsonSchema\Exception\RuntimeException; use JsonSchema\Validator; /** @@ -23,7 +24,8 @@ class Curl extends AbstractRetriever public function __construct() { if (!function_exists('curl_init')) { - throw new \RuntimeException('cURL not installed'); + // Cannot test this, because curl_init is present on all test platforms plus mock + throw new RuntimeException('cURL not installed'); // @codeCoverageIgnore } } diff --git a/src/JsonSchema/Uri/Retrievers/FileGetContents.php b/src/JsonSchema/Uri/Retrievers/FileGetContents.php index 7f0c399a..7019814f 100644 --- a/src/JsonSchema/Uri/Retrievers/FileGetContents.php +++ b/src/JsonSchema/Uri/Retrievers/FileGetContents.php @@ -50,8 +50,10 @@ public function retrieve($uri) $this->messageBody = $response; if (!empty($http_response_header)) { - $this->fetchContentType($http_response_header); - } else { + // $http_response_header cannot be tested, because it's defined in the method's local scope + // See http://php.net/manual/en/reserved.variables.httpresponseheader.php for more info. + $this->fetchContentType($http_response_header); // @codeCoverageIgnore + } else { // @codeCoverageIgnore // Could be a "file://" url or something else - fake up the response $this->contentType = null; } diff --git a/src/JsonSchema/Uri/UriRetriever.php b/src/JsonSchema/Uri/UriRetriever.php index ebb7eb33..65452788 100644 --- a/src/JsonSchema/Uri/UriRetriever.php +++ b/src/JsonSchema/Uri/UriRetriever.php @@ -24,6 +24,14 @@ */ class UriRetriever implements BaseUriRetrieverInterface { + /** + * @var array Map of URL translations + */ + protected $translationMap = array( + // use local copies of the spec schemas + '|^https?://json-schema.org/draft-(0[34])/schema#?|' => 'package://dist/schema/json-schema-draft-$1.json' + ); + /** * @var null|UriRetrieverInterface */ @@ -134,7 +142,7 @@ public function resolvePointer($jsonSchema, $uri) /** * {@inheritdoc} */ - public function retrieve($uri, $baseUri = null) + public function retrieve($uri, $baseUri = null, $translate = true) { $resolver = new UriResolver(); $resolvedUri = $fetchUri = $resolver->resolve($uri, $baseUri); @@ -146,6 +154,11 @@ public function retrieve($uri, $baseUri = null) $fetchUri = $resolver->generate($arParts); } + // apply URI translations + if ($translate) { + $fetchUri = $this->translate($fetchUri); + } + $jsonSchema = $this->loadSchema($fetchUri); // Use the JSON pointer if specified @@ -291,4 +304,27 @@ public function isValid($uri) return !empty($components); } + + /** + * Set a URL translation rule + */ + public function setTranslation($from, $to) + { + $this->translationMap[$from] = $to; + } + + /** + * Apply URI translation rules + */ + public function translate($uri) + { + foreach ($this->translationMap as $from => $to) { + $uri = preg_replace($from, $to, $uri); + } + + // translate references to local files within the json-schema package + $uri = preg_replace('|^package://|', sprintf('file://%s/', realpath(__DIR__ . '/../../..')), $uri); + + return $uri; + } } diff --git a/src/JsonSchema/Validator.php b/src/JsonSchema/Validator.php index e2a919bc..9554175f 100644 --- a/src/JsonSchema/Validator.php +++ b/src/JsonSchema/Validator.php @@ -12,6 +12,7 @@ use JsonSchema\Constraints\BaseConstraint; use JsonSchema\Constraints\Constraint; use JsonSchema\Exception\InvalidConfigException; +use JsonSchema\SchemaStorage; /** * A JsonSchema Constraint @@ -25,6 +26,11 @@ class Validator extends BaseConstraint { const SCHEMA_MEDIA_TYPE = 'application/schema+json'; + const ERROR_NONE = 0x00000000; + const ERROR_ALL = 0xFFFFFFFF; + const ERROR_DOCUMENT_VALIDATION = 0x00000001; + const ERROR_SCHEMA_VALIDATION = 0x00000002; + /** * Validates the given data against the schema and returns an object containing the results * Both the php object and the schema are supposed to be a result of a json_decode call. @@ -36,17 +42,28 @@ class Validator extends BaseConstraint */ public function validate(&$value, $schema = null, $checkMode = null) { + // make sure $schema is an object + if (is_array($schema)) { + $schema = self::arrayToObjectRecursive($schema); + } + + // set checkMode $initialCheckMode = $this->factory->getConfig(); if ($checkMode !== null) { $this->factory->setConfig($checkMode); } + // add provided schema to SchemaStorage with internal URI to allow internal $ref resolution + $this->factory->getSchemaStorage()->addSchema(SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI, $schema); + $validator = $this->factory->createInstanceFor('schema'); $validator->check($value, $schema); $this->factory->setConfig($initialCheckMode); $this->addErrors(array_unique($validator->getErrors(), SORT_REGULAR)); + + return $validator->getErrorMask(); } /** diff --git a/tests/Constraints/AdditionalPropertiesTest.php b/tests/Constraints/AdditionalPropertiesTest.php index 4d68654b..e9831964 100644 --- a/tests/Constraints/AdditionalPropertiesTest.php +++ b/tests/Constraints/AdditionalPropertiesTest.php @@ -9,8 +9,12 @@ namespace JsonSchema\Tests\Constraints; +use JsonSchema\Validator; + class AdditionalPropertiesTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( @@ -37,6 +41,7 @@ public function getInvalidTests() 'pointer' => '', 'message' => 'The property additionalProp is not defined and the definition does not allow additional properties', 'constraint' => 'additionalProp', + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ) ) ), diff --git a/tests/Constraints/ArraysTest.php b/tests/Constraints/ArraysTest.php index 5498e35e..dac14358 100644 --- a/tests/Constraints/ArraysTest.php +++ b/tests/Constraints/ArraysTest.php @@ -11,6 +11,8 @@ class ArraysTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/BaseTestCase.php b/tests/Constraints/BaseTestCase.php index 75010e33..50efbd82 100644 --- a/tests/Constraints/BaseTestCase.php +++ b/tests/Constraints/BaseTestCase.php @@ -20,19 +20,31 @@ */ abstract class BaseTestCase extends VeryBaseTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-04/schema#'; + protected $validateSchema = false; + /** * @dataProvider getInvalidTests */ public function testInvalidCases($input, $schema, $checkMode = Constraint::CHECK_MODE_NORMAL, $errors = array()) { $checkMode = $checkMode === null ? Constraint::CHECK_MODE_NORMAL : $checkMode; + if ($this->validateSchema) { + $checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA; + } $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + if (is_object($schema) && !isset($schema->{'$schema'})) { + $schema->{'$schema'} = $this->schemaSpec; + } $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); $checkValue = json_decode($input); - $validator->validate($checkValue, $schema); + $errorMask = $validator->validate($checkValue, $schema); + + $this->assertTrue((bool) ($errorMask & Validator::ERROR_DOCUMENT_VALIDATION)); + $this->assertGreaterThan(0, $validator->numErrors()); if (array() !== $errors) { $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(), true)); @@ -46,16 +58,25 @@ public function testInvalidCases($input, $schema, $checkMode = Constraint::CHECK public function testInvalidCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_TYPE_CAST, $errors = array()) { $checkMode = $checkMode === null ? Constraint::CHECK_MODE_TYPE_CAST : $checkMode; - if ($checkMode !== Constraint::CHECK_MODE_TYPE_CAST) { + if ($this->validateSchema) { + $checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA; + } + if (!($checkMode & Constraint::CHECK_MODE_TYPE_CAST)) { $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_TYPE_CAST"'); } $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + if (is_object($schema) && !isset($schema->{'$schema'})) { + $schema->{'$schema'} = $this->schemaSpec; + } $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); $checkValue = json_decode($input, true); - $validator->validate($checkValue, $schema); + $errorMask = $validator->validate($checkValue, $schema); + + $this->assertTrue((bool) ($errorMask & Validator::ERROR_DOCUMENT_VALIDATION)); + $this->assertGreaterThan(0, $validator->numErrors()); if (array() !== $errors) { $this->assertEquals($errors, $validator->getErrors(), print_r($validator->getErrors(), true)); @@ -68,12 +89,19 @@ public function testInvalidCasesUsingAssoc($input, $schema, $checkMode = Constra */ public function testValidCases($input, $schema, $checkMode = Constraint::CHECK_MODE_NORMAL) { + if ($this->validateSchema) { + $checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA; + } $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + if (is_object($schema) && !isset($schema->{'$schema'})) { + $schema->{'$schema'} = $this->schemaSpec; + } $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); $checkValue = json_decode($input); - $validator->validate($checkValue, $schema); + $errorMask = $validator->validate($checkValue, $schema); + $this->assertEquals(0, $errorMask); $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); } @@ -83,18 +111,25 @@ public function testValidCases($input, $schema, $checkMode = Constraint::CHECK_M */ public function testValidCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_TYPE_CAST) { - if ($checkMode !== Constraint::CHECK_MODE_TYPE_CAST) { + if ($this->validateSchema) { + $checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA; + } + if (!($checkMode & Constraint::CHECK_MODE_TYPE_CAST)) { $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_TYPE_CAST"'); } $schema = json_decode($schema); $schemaStorage = new SchemaStorage($this->getUriRetrieverMock($schema), new UriResolver()); $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + if (is_object($schema) && !isset($schema->{'$schema'})) { + $schema->{'$schema'} = $this->schemaSpec; + } $value = json_decode($input, true); $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); - $validator->validate($value, $schema); + $errorMask = $validator->validate($value, $schema); + $this->assertEquals(0, $errorMask); $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); } diff --git a/tests/Constraints/BasicTypesTest.php b/tests/Constraints/BasicTypesTest.php index 7daa43ad..0e88ef42 100644 --- a/tests/Constraints/BasicTypesTest.php +++ b/tests/Constraints/BasicTypesTest.php @@ -11,6 +11,9 @@ class BasicTypesTest extends BaseTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/CoerciveTest.php b/tests/Constraints/CoerciveTest.php index cb5c5518..e4dd173d 100644 --- a/tests/Constraints/CoerciveTest.php +++ b/tests/Constraints/CoerciveTest.php @@ -17,6 +17,9 @@ class CoerciveTest extends BasicTypesTest { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + /** * @dataProvider getValidCoerceTests */ @@ -116,6 +119,15 @@ public function testInvalidCoerceCasesUsingAssoc($input, $schema, $errors = arra $this->assertFalse($validator->isValid(), print_r($validator->getErrors(), true)); } + public function testCoerceAPI() + { + $input = json_decode('{"propertyOne": "10"}'); + $schema = json_decode('{"properties":{"propertyOne":{"type":"number"}}}'); + $v = new Validator(); + $v->coerce($input, $schema); + $this->assertEquals('{"propertyOne":10}', json_encode($input)); + } + public function getValidCoerceTests() { return array( diff --git a/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php index 6687e7c2..a3b9c4e2 100644 --- a/tests/Constraints/DefaultPropertiesTest.php +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -19,83 +19,143 @@ class DefaultPropertiesTest extends VeryBaseTestCase public function getValidTests() { return array( - array(// default value for entire object + /* + // This test case was intended to check whether a default value can be applied for the + // entire object, however testing this case is impossible, because there is no way to + // distinguish between a deliberate top-level NULL and a top level that contains nothing. + // As such, the assumption is that a top-level NULL is deliberate, and should not be + // altered by replacing it with a default value. + array(// #0 default value for entire object '', '{"default":"valueOne"}', '"valueOne"' ), - array(// default value in an empty object + */ + array(// #0 default value in an empty object '{}', '{"properties":{"propertyOne":{"default":"valueOne"}}}', '{"propertyOne":"valueOne"}' ), - array(// default value for top-level property + array(// #1 default value for top-level property '{"propertyOne":"valueOne"}', '{"properties":{"propertyTwo":{"default":"valueTwo"}}}', '{"propertyOne":"valueOne","propertyTwo":"valueTwo"}' ), - array(// default value for sub-property + array(// #2 default value for sub-property '{"propertyOne":{}}', '{"properties":{"propertyOne":{"properties":{"propertyTwo":{"default":"valueTwo"}}}}}', '{"propertyOne":{"propertyTwo":"valueTwo"}}' ), - array(// default value for sub-property with sibling + array(// #3 default value for sub-property with sibling '{"propertyOne":{"propertyTwo":"valueTwo"}}', '{"properties":{"propertyOne":{"properties":{"propertyThree":{"default":"valueThree"}}}}}', '{"propertyOne":{"propertyTwo":"valueTwo","propertyThree":"valueThree"}}' ), - array(// default value for top-level property with type check + array(// #4 default value for top-level property with type check '{"propertyOne":"valueOne"}', '{"properties":{"propertyTwo":{"default":"valueTwo","type":"string"}}}', '{"propertyOne":"valueOne","propertyTwo":"valueTwo"}' ), - array(// default value for top-level property with v3 required check + array(// #5 default value for top-level property with v3 required check '{"propertyOne":"valueOne"}', '{"properties":{"propertyTwo":{"default":"valueTwo","required":"true"}}}', '{"propertyOne":"valueOne","propertyTwo":"valueTwo"}' ), - array(// default value for top-level property with v4 required check + array(// #6 default value for top-level property with v4 required check '{"propertyOne":"valueOne"}', '{"properties":{"propertyTwo":{"default":"valueTwo"}},"required":["propertyTwo"]}', '{"propertyOne":"valueOne","propertyTwo":"valueTwo"}' ), - array(//default value for an already set property + array(// #7 default value for an already set property '{"propertyOne":"alreadySetValueOne"}', '{"properties":{"propertyOne":{"default":"valueOne"}}}', '{"propertyOne":"alreadySetValueOne"}' ), - array(//default item value for an array + array(// #8 default item value for an array '["valueOne"]', '{"type":"array","items":[{},{"type":"string","default":"valueTwo"}]}', '["valueOne","valueTwo"]' ), - array(//default item value for an empty array + array(// #9 default item value for an empty array '[]', '{"type":"array","items":[{"type":"string","default":"valueOne"}]}', '["valueOne"]' ), - array(//property without a default available + array(// #10 property without a default available '{"propertyOne":"alreadySetValueOne"}', '{"properties":{"propertyOne":{"type":"string"}}}', '{"propertyOne":"alreadySetValueOne"}' ), - array(// default property value is an object + array(// #11 default property value is an object '{"propertyOne":"valueOne"}', '{"properties":{"propertyTwo":{"default":{}}}}', '{"propertyOne":"valueOne","propertyTwo":{}}' ), - array(// default item value is an object + array(// #12 default item value is an object '[]', '{"type":"array","items":[{"default":{}}]}', '[{}]' - ) + ), + array(// #13 only set required values (draft-04) + '{}', + '{ + "properties": { + "propertyOne": {"default": "valueOne"}, + "propertyTwo": {"default": "valueTwo"} + }, + "required": ["propertyTwo"] + }', + '{"propertyTwo":"valueTwo"}', + Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS + ), + array(// #14 only set required values (draft-03) + '{}', + '{ + "properties": { + "propertyOne": {"default": "valueOne"}, + "propertyTwo": {"default": "valueTwo", "required": true} + } + }', + '{"propertyTwo":"valueTwo"}', + Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS + ), + array(// #15 infinite recursion via $ref (object) + '{}', + '{"properties":{"propertyOne": {"$ref": "#","default": {}}}}', + '{"propertyOne":{}}' + ), + array(// #16 infinite recursion via $ref (array) + '[]', + '{"items":[{"$ref":"#","default":[]}]}', + '[[]]' + ), + array(// #17 default top value does not overwrite defined null + 'null', + '{"default":"valueOne"}', + 'null' + ), + array(// #18 default property value does not overwrite defined null + '{"propertyOne":null}', + '{"properties":{"propertyOne":{"default":"valueOne"}}}', + '{"propertyOne":null}' + ), + array(// #19 default value in an object is null + '{}', + '{"properties":{"propertyOne":{"default":null}}}', + '{"propertyOne":null}' + ), + array(// #20 default value in an array is null + '[]', + '{"items":[{"default":null}]}', + '[null]' + ), ); } /** * @dataProvider getValidTests */ - public function testValidCases($input, $schema, $expectOutput = null, $validator = null) + public function testValidCases($input, $schema, $expectOutput = null, $checkMode = 0) { if (is_string($input)) { $inputDecoded = json_decode($input); @@ -103,11 +163,14 @@ public function testValidCases($input, $schema, $expectOutput = null, $validator $inputDecoded = $input; } - if ($validator === null) { - $factory = new Factory(null, null, Constraint::CHECK_MODE_APPLY_DEFAULTS); - $validator = new Validator($factory); - } - $validator->validate($inputDecoded, json_decode($schema)); + $checkMode |= Constraint::CHECK_MODE_APPLY_DEFAULTS; + + $schemaStorage = new SchemaStorage(); + $schemaStorage->addSchema('local://testSchema', json_decode($schema)); + $factory = new Factory($schemaStorage); + $validator = new Validator($factory); + + $validator->validate($inputDecoded, json_decode('{"$ref": "local://testSchema"}'), $checkMode); $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); @@ -119,28 +182,28 @@ public function testValidCases($input, $schema, $expectOutput = null, $validator /** * @dataProvider getValidTests */ - public function testValidCasesUsingAssoc($input, $schema, $expectOutput = null) + public function testValidCasesUsingAssoc($input, $schema, $expectOutput = null, $checkMode = 0) { $input = json_decode($input, true); - $factory = new Factory(null, null, Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_APPLY_DEFAULTS); - self::testValidCases($input, $schema, $expectOutput, new Validator($factory)); + $checkMode |= Constraint::CHECK_MODE_TYPE_CAST; + self::testValidCases($input, $schema, $expectOutput, $checkMode); } /** * @dataProvider getValidTests */ - public function testValidCasesUsingAssocWithoutTypeCast($input, $schema, $expectOutput = null) + public function testValidCasesUsingAssocWithoutTypeCast($input, $schema, $expectOutput = null, $checkMode = 0) { $input = json_decode($input, true); - $factory = new Factory(null, null, Constraint::CHECK_MODE_APPLY_DEFAULTS); - self::testValidCases($input, $schema, $expectOutput, new Validator($factory)); + + self::testValidCases($input, $schema, $expectOutput, $checkMode); } public function testNoModificationViaReferences() { - $input = json_decode(''); - $schema = json_decode('{"default":{"propertyOne":"valueOne"}}'); + $input = json_decode('{}'); + $schema = json_decode('{"properties":{"propertyOne":{"default":"valueOne"}}}'); $validator = new Validator(); $validator->validate($input, $schema, Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_APPLY_DEFAULTS); @@ -148,6 +211,21 @@ public function testNoModificationViaReferences() $this->assertEquals('{"propertyOne":"valueOne"}', json_encode($input)); $input->propertyOne = 'valueTwo'; - $this->assertEquals('valueOne', $schema->default->propertyOne); + $this->assertEquals('valueOne', $schema->properties->propertyOne->default); + } + + public function testLeaveBasicTypesAlone() + { + $input = json_decode('"ThisIsAString"'); + $schema = json_decode('{"properties": {"propertyOne": {"default": "valueOne"}}}'); + + $validator = new Validator(); + $validator->validate($input, $schema, Constraint::CHECK_MODE_APPLY_DEFAULTS); + + $this->assertEquals('"ThisIsAString"', json_encode($input)); + + $schema = json_decode('{"items":[{"type":"string","default":"valueOne"}]}'); + $validator->validate($input, $schema, Constraint::CHECK_MODE_APPLY_DEFAULTS); + $this->assertEquals('"ThisIsAString"', json_encode($input)); } } diff --git a/tests/Constraints/DependenciesTest.php b/tests/Constraints/DependenciesTest.php index 2e508218..f7f9d532 100644 --- a/tests/Constraints/DependenciesTest.php +++ b/tests/Constraints/DependenciesTest.php @@ -11,6 +11,9 @@ class DependenciesTest extends BaseTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/DisallowTest.php b/tests/Constraints/DisallowTest.php index 07446d9d..f00d2fab 100644 --- a/tests/Constraints/DisallowTest.php +++ b/tests/Constraints/DisallowTest.php @@ -11,6 +11,14 @@ class DisallowTest extends BaseTestCase { + // schemas in these tests look like draft-03, but the 'disallow' patterns provided are in + // violation of the spec - 'disallow' as defined in draft-03 accepts the same values as the + // 'type' option, and cannot take arbitrary patterns. The implementation in this library is + // probably deliberate, but noting that it's invalid, schema validation has been disabled + // for these tests. The 'disallow' option was removed permanently in draft-04. + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = false; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/DivisibleByTest.php b/tests/Constraints/DivisibleByTest.php index 8a010965..b88a87a1 100644 --- a/tests/Constraints/DivisibleByTest.php +++ b/tests/Constraints/DivisibleByTest.php @@ -11,6 +11,8 @@ class DivisibleByTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/EnumTest.php b/tests/Constraints/EnumTest.php index 0ca5b9e2..723321d0 100644 --- a/tests/Constraints/EnumTest.php +++ b/tests/Constraints/EnumTest.php @@ -11,6 +11,9 @@ class EnumTest extends BaseTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/ExtendsTest.php b/tests/Constraints/ExtendsTest.php index 289484f3..5df1fa27 100644 --- a/tests/Constraints/ExtendsTest.php +++ b/tests/Constraints/ExtendsTest.php @@ -11,6 +11,9 @@ class ExtendsTest extends BaseTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/FormatTest.php b/tests/Constraints/FormatTest.php index 73de5784..ad7075d9 100644 --- a/tests/Constraints/FormatTest.php +++ b/tests/Constraints/FormatTest.php @@ -9,10 +9,14 @@ namespace JsonSchema\Tests\Constraints; +use JsonSchema\Constraints\Constraint; +use JsonSchema\Constraints\Factory; use JsonSchema\Constraints\FormatConstraint; class FormatTest extends BaseTestCase { + protected $validateSchema = true; + public function setUp() { date_default_timezone_set('UTC'); @@ -76,6 +80,21 @@ public function testInvalidFormat($string, $format) $this->assertEquals(1, count($validator->getErrors()), 'Expected 1 error'); } + /** + * @dataProvider getInvalidFormats + */ + public function testDisabledFormat($string, $format) + { + $factory = new Factory(); + $validator = new FormatConstraint($factory); + $schema = new \stdClass(); + $schema->format = $format; + $factory->addConfig(Constraint::CHECK_MODE_DISABLE_FORMAT); + + $validator->check($string, $schema); + $this->assertEmpty($validator->getErrors()); + } + public function getValidFormats() { return array( diff --git a/tests/Constraints/LongArraysTest.php b/tests/Constraints/LongArraysTest.php index 849c0371..2757b964 100644 --- a/tests/Constraints/LongArraysTest.php +++ b/tests/Constraints/LongArraysTest.php @@ -15,6 +15,8 @@ class LongArraysTest extends VeryBaseTestCase { + protected $validateSchema = true; + public function testLongStringArray() { $schema = diff --git a/tests/Constraints/MinItemsMaxItemsTest.php b/tests/Constraints/MinItemsMaxItemsTest.php index 1b477845..62fbaa9a 100644 --- a/tests/Constraints/MinItemsMaxItemsTest.php +++ b/tests/Constraints/MinItemsMaxItemsTest.php @@ -11,6 +11,8 @@ class MinItemsMaxItemsTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/MinLengthMaxLengthMultiByteTest.php b/tests/Constraints/MinLengthMaxLengthMultiByteTest.php index ab110a40..b19ec4f7 100644 --- a/tests/Constraints/MinLengthMaxLengthMultiByteTest.php +++ b/tests/Constraints/MinLengthMaxLengthMultiByteTest.php @@ -11,6 +11,8 @@ class MinLengthMaxLengthMultiByteTest extends BaseTestCase { + protected $validateSchema = true; + protected function setUp() { if (!extension_loaded('mbstring')) { diff --git a/tests/Constraints/MinLengthMaxLengthTest.php b/tests/Constraints/MinLengthMaxLengthTest.php index 0e09a7a3..8dfa7158 100644 --- a/tests/Constraints/MinLengthMaxLengthTest.php +++ b/tests/Constraints/MinLengthMaxLengthTest.php @@ -11,6 +11,8 @@ class MinLengthMaxLengthTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/MinMaxPropertiesTest.php b/tests/Constraints/MinMaxPropertiesTest.php index 8c3a641d..e4fd5a1a 100644 --- a/tests/Constraints/MinMaxPropertiesTest.php +++ b/tests/Constraints/MinMaxPropertiesTest.php @@ -11,6 +11,8 @@ class MinMaxPropertiesTest extends BaseTestCase { + protected $validateSchema = true; + /** * {@inheritdoc} */ diff --git a/tests/Constraints/MinimumMaximumTest.php b/tests/Constraints/MinimumMaximumTest.php index c25a7c29..508c0253 100644 --- a/tests/Constraints/MinimumMaximumTest.php +++ b/tests/Constraints/MinimumMaximumTest.php @@ -11,6 +11,8 @@ class MinimumMaximumTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/NotTest.php b/tests/Constraints/NotTest.php index 27f02225..3a950f57 100644 --- a/tests/Constraints/NotTest.php +++ b/tests/Constraints/NotTest.php @@ -11,6 +11,8 @@ class NotTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/NumberAndIntegerTypesTest.php b/tests/Constraints/NumberAndIntegerTypesTest.php index 91e1c7cb..6c7277b9 100644 --- a/tests/Constraints/NumberAndIntegerTypesTest.php +++ b/tests/Constraints/NumberAndIntegerTypesTest.php @@ -11,6 +11,8 @@ class NumberAndIntegerTypesTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/OfPropertiesTest.php b/tests/Constraints/OfPropertiesTest.php index c36ba29e..17623f49 100644 --- a/tests/Constraints/OfPropertiesTest.php +++ b/tests/Constraints/OfPropertiesTest.php @@ -8,11 +8,16 @@ namespace JsonSchema\Tests\Constraints; +use JsonSchema\Constraints\Constraint; +use JsonSchema\Validator; + /** * Class OfPropertiesTest */ class OfPropertiesTest extends BaseTestCase { + protected $validateSchema = true; + public function getValidTests() { return array( @@ -76,18 +81,21 @@ public function getInvalidTests() 'pointer' => '/prop2', 'message' => 'Array value found, but a string is required', 'constraint' => 'type', + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), array( 'property' => 'prop2', 'pointer' => '/prop2', 'message' => 'Array value found, but a number is required', 'constraint' => 'type', + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), array( 'property' => 'prop2', 'pointer' => '/prop2', 'message' => 'Failed to match exactly one schema', 'constraint' => 'oneOf', + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), ), ), @@ -208,4 +216,44 @@ public function getInvalidTests() ) ); } + + public function testNoPrematureAnyOfException() + { + $schema = json_decode('{ + "type": "object", + "properties": { + "propertyOne": { + "anyOf": [ + {"type": "number"}, + {"type": "string"} + ] + } + } + }'); + $data = json_decode('{"propertyOne":"ABC"}'); + + $v = new Validator(); + $v->validate($data, $schema, Constraint::CHECK_MODE_EXCEPTIONS); + $this->assertTrue($v->isValid()); + } + + public function testNoPrematureOneOfException() + { + $schema = json_decode('{ + "type": "object", + "properties": { + "propertyOne": { + "oneOf": [ + {"type": "number"}, + {"type": "string"} + ] + } + } + }'); + $data = json_decode('{"propertyOne":"ABC"}'); + + $v = new Validator(); + $v->validate($data, $schema, Constraint::CHECK_MODE_EXCEPTIONS); + $this->assertTrue($v->isValid()); + } } diff --git a/tests/Constraints/PatternPropertiesTest.php b/tests/Constraints/PatternPropertiesTest.php index a04e45b9..8dede058 100644 --- a/tests/Constraints/PatternPropertiesTest.php +++ b/tests/Constraints/PatternPropertiesTest.php @@ -11,6 +11,8 @@ class PatternPropertiesTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/PatternTest.php b/tests/Constraints/PatternTest.php index 0f69b9ad..c017600c 100644 --- a/tests/Constraints/PatternTest.php +++ b/tests/Constraints/PatternTest.php @@ -11,6 +11,8 @@ class PatternTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/PointerTest.php b/tests/Constraints/PointerTest.php index ca378e3d..88e5247d 100644 --- a/tests/Constraints/PointerTest.php +++ b/tests/Constraints/PointerTest.php @@ -13,6 +13,8 @@ class PointerTest extends \PHPUnit_Framework_TestCase { + protected $validateSchema = true; + public function testVariousPointers() { $schema = array( @@ -88,25 +90,29 @@ public function testVariousPointers() 'property' => 'prop1', 'pointer' => '/prop1', 'message' => 'The property prop1 is required', - 'constraint' => 'required' + 'constraint' => 'required', + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), array( 'property' => 'prop2.prop2.1', 'pointer' => '/prop2/prop2.1', 'message' => 'The property prop2.1 is required', - 'constraint' => 'required' + 'constraint' => 'required', + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), array( 'property' => 'prop3.prop3/1.prop3/1.1', 'pointer' => '/prop3/prop3~11/prop3~11.1', 'message' => 'The property prop3/1.1 is required', - 'constraint' => 'required' + 'constraint' => 'required', + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ), array( 'property' => 'prop4[0].prop4-child', 'pointer' => '/prop4/0/prop4-child', 'message' => 'The property prop4-child is required', - 'constraint' => 'required' + 'constraint' => 'required', + 'context' => Validator::ERROR_DOCUMENT_VALIDATION ) ), $validator->getErrors() diff --git a/tests/Constraints/ReadOnlyTest.php b/tests/Constraints/ReadOnlyTest.php index 7a3e8678..23434406 100644 --- a/tests/Constraints/ReadOnlyTest.php +++ b/tests/Constraints/ReadOnlyTest.php @@ -11,6 +11,8 @@ class ReadOnlyTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { //is readonly really required? diff --git a/tests/Constraints/RequireTest.php b/tests/Constraints/RequireTest.php index c10f8a7b..efb6f63e 100644 --- a/tests/Constraints/RequireTest.php +++ b/tests/Constraints/RequireTest.php @@ -11,6 +11,8 @@ class RequireTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/RequiredPropertyTest.php b/tests/Constraints/RequiredPropertyTest.php index 10d42f57..31545a64 100644 --- a/tests/Constraints/RequiredPropertyTest.php +++ b/tests/Constraints/RequiredPropertyTest.php @@ -14,6 +14,12 @@ class RequiredPropertyTest extends BaseTestCase { + // Most tests are draft-03 compliant, but some tests are draft-04, or mix draft-03 and + // draft-04 syntax within the same schema. Unfortunately, draft-03 and draft-04 required + // definitions are incompatible, so disabling schema validation for these tests. + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = false; + public function testErrorPropertyIsPopulatedForRequiredIfMissingInInput() { $validator = new UndefinedConstraint(); diff --git a/tests/Constraints/SchemaValidationTest.php b/tests/Constraints/SchemaValidationTest.php new file mode 100644 index 00000000..356637ae --- /dev/null +++ b/tests/Constraints/SchemaValidationTest.php @@ -0,0 +1,124 @@ +validate($input, $schema, Constraint::CHECK_MODE_VALIDATE_SCHEMA); + + $this->assertTrue((bool) (Validator::ERROR_SCHEMA_VALIDATION & $errorMask)); + $this->assertGreaterThan(0, $v->numErrors(Validator::ERROR_SCHEMA_VALIDATION)); + $this->assertEquals(0, $v->numErrors(Validator::ERROR_DOCUMENT_VALIDATION)); + + $this->assertFalse($v->isValid(), 'Validation succeeded for an invalid test case'); + foreach ($v->getErrors() as $error) { + $this->assertEquals(Validator::ERROR_SCHEMA_VALIDATION, $error['context']); + } + } + + /** + * @dataProvider getValidTests + */ + public function testValidCases($schema) + { + $input = json_decode('{"propertyOne":"valueOne"}'); + $schema = json_decode($schema); + + $v = new Validator(); + $errorMask = $v->validate($input, $schema, Constraint::CHECK_MODE_VALIDATE_SCHEMA); + $this->assertEquals(0, $errorMask); + + if (!$v->isValid()) { + var_dump($v->getErrors(Validator::ERROR_SCHEMA_VALIDATION)); + } + $this->assertTrue($v->isValid(), 'Validation failed on a valid test case'); + } + + public function testNonObjectSchema() + { + $this->setExpectedException( + '\JsonSchema\Exception\RuntimeException', + 'Cannot validate the schema of a non-object' + ); + $this->testValidCases('"notAnObject"'); + } + + public function testInvalidSchemaException() + { + $this->setExpectedException( + '\JsonSchema\Exception\InvalidSchemaException', + 'Schema did not pass validation' + ); + + $input = json_decode('{}'); + $schema = json_decode('{"properties":{"propertyOne":{"type":"string","required":true}}}'); + + $v = new Validator(); + $v->validate($input, $schema, Constraint::CHECK_MODE_VALIDATE_SCHEMA | Constraint::CHECK_MODE_EXCEPTIONS); + } +} diff --git a/tests/Constraints/SelfDefinedSchemaTest.php b/tests/Constraints/SelfDefinedSchemaTest.php index d2cce50e..e7d3d70b 100644 --- a/tests/Constraints/SelfDefinedSchemaTest.php +++ b/tests/Constraints/SelfDefinedSchemaTest.php @@ -13,12 +13,15 @@ class SelfDefinedSchemaTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( array( '{ "$schema": { + "$schema": "http://json-schema.org/draft-04/schema#", "properties": { "name": { "type": "string" @@ -44,6 +47,7 @@ public function getValidTests() array( '{ "$schema": { + "$schema": "http://json-schema.org/draft-04/schema#", "properties": { "name": { "type": "string" diff --git a/tests/Constraints/TupleTypingTest.php b/tests/Constraints/TupleTypingTest.php index ceab8ec6..08fedc0a 100644 --- a/tests/Constraints/TupleTypingTest.php +++ b/tests/Constraints/TupleTypingTest.php @@ -11,6 +11,8 @@ class TupleTypingTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/TypeTest.php b/tests/Constraints/TypeTest.php index df8d6dd1..24138478 100644 --- a/tests/Constraints/TypeTest.php +++ b/tests/Constraints/TypeTest.php @@ -92,4 +92,31 @@ private function assertTypeConstraintError($expected, TypeConstraint $actual) $this->assertEquals($expected, $actualMessage); // first equal for the diff $this->assertSame($expected, $actualMessage); // the same for the strictness } + + public function testValidateTypeNameWording() + { + $t = new TypeConstraint(); + $r = new \ReflectionObject($t); + $m = $r->getMethod('validateTypeNameWording'); + $m->setAccessible(true); + + $this->setExpectedException( + '\UnexpectedValueException', + "No wording for 'notAValidTypeName' available, expected wordings are: [an integer, a number, a boolean, an object, an array, a string, a null]" + ); + $m->invoke($t, 'notAValidTypeName'); + } + + public function testValidateTypeException() + { + $t = new TypeConstraint(); + $data = new \StdClass(); + $schema = json_decode('{"type": "notAValidTypeName"}'); + + $this->setExpectedException( + 'JsonSchema\Exception\InvalidArgumentException', + 'object is an invalid type for notAValidTypeName' + ); + $t->check($data, $schema); + } } diff --git a/tests/Constraints/UnionTypesTest.php b/tests/Constraints/UnionTypesTest.php index 01e49c4e..42676308 100644 --- a/tests/Constraints/UnionTypesTest.php +++ b/tests/Constraints/UnionTypesTest.php @@ -11,6 +11,8 @@ class UnionTypesTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/UnionWithNullValueTest.php b/tests/Constraints/UnionWithNullValueTest.php index a077cfdf..60301f2e 100644 --- a/tests/Constraints/UnionWithNullValueTest.php +++ b/tests/Constraints/UnionWithNullValueTest.php @@ -11,6 +11,8 @@ class UnionWithNullValueTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/UniqueItemsTest.php b/tests/Constraints/UniqueItemsTest.php index 4abac569..099b407c 100644 --- a/tests/Constraints/UniqueItemsTest.php +++ b/tests/Constraints/UniqueItemsTest.php @@ -11,6 +11,8 @@ class UniqueItemsTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Constraints/VeryBaseTestCase.php b/tests/Constraints/VeryBaseTestCase.php index 7d8eb267..7cc0d1c6 100644 --- a/tests/Constraints/VeryBaseTestCase.php +++ b/tests/Constraints/VeryBaseTestCase.php @@ -66,7 +66,7 @@ private function getJsonSchemaDraft03() { if (!$this->jsonSchemaDraft03) { $this->jsonSchemaDraft03 = json_decode( - file_get_contents(__DIR__ . '/../fixtures/json-schema-draft-03.json') + file_get_contents(__DIR__ . '/../../dist/schema/json-schema-draft-03.json') ); } @@ -80,7 +80,7 @@ private function getJsonSchemaDraft04() { if (!$this->jsonSchemaDraft04) { $this->jsonSchemaDraft04 = json_decode( - file_get_contents(__DIR__ . '/../fixtures/json-schema-draft-04.json') + file_get_contents(__DIR__ . '/../../dist/schema/json-schema-draft-04.json') ); } diff --git a/tests/Constraints/WrongMessagesFailingTestCaseTest.php b/tests/Constraints/WrongMessagesFailingTestCaseTest.php index ca620420..80a14421 100644 --- a/tests/Constraints/WrongMessagesFailingTestCaseTest.php +++ b/tests/Constraints/WrongMessagesFailingTestCaseTest.php @@ -11,6 +11,8 @@ class WrongMessagesFailingTestCaseTest extends BaseTestCase { + protected $validateSchema = true; + public function getInvalidTests() { return array( diff --git a/tests/Drafts/Draft3Test.php b/tests/Drafts/Draft3Test.php index 4a744441..1942a3b1 100644 --- a/tests/Drafts/Draft3Test.php +++ b/tests/Drafts/Draft3Test.php @@ -16,6 +16,9 @@ */ class Draft3Test extends BaseDraftTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-03/schema#'; + protected $validateSchema = true; + /** * {@inheritdoc} */ diff --git a/tests/Drafts/Draft4Test.php b/tests/Drafts/Draft4Test.php index a4508b0f..54eee4c4 100644 --- a/tests/Drafts/Draft4Test.php +++ b/tests/Drafts/Draft4Test.php @@ -14,6 +14,9 @@ */ class Draft4Test extends BaseDraftTestCase { + protected $schemaSpec = 'http://json-schema.org/draft-04/schema#'; + protected $validateSchema = true; + /** * {@inheritdoc} */ diff --git a/tests/Entity/JsonPointerTest.php b/tests/Entity/JsonPointerTest.php index 6a5ff4bf..65859895 100644 --- a/tests/Entity/JsonPointerTest.php +++ b/tests/Entity/JsonPointerTest.php @@ -109,4 +109,13 @@ public function testJsonPointerWithPropertyPaths() $this->assertEquals(array('~definitions/general', '%custom%'), $modified->getPropertyPaths()); $this->assertEquals('#/~0definitions~1general/%25custom%25', $modified->getPropertyPathAsString()); } + + public function testCreateWithInvalidValue() + { + $this->setExpectedException( + '\JsonSchema\Exception\InvalidArgumentException', + 'Ref value must be a string' + ); + new JsonPointer(null); + } } diff --git a/tests/Iterators/ObjectIteratorTest.php b/tests/Iterators/ObjectIteratorTest.php new file mode 100644 index 00000000..703df833 --- /dev/null +++ b/tests/Iterators/ObjectIteratorTest.php @@ -0,0 +1,89 @@ +testObject = (object) array( + 'subOne' => (object) array( + 'propertyOne' => 'valueOne', + 'propertyTwo' => 'valueTwo', + 'propertyThree' => 'valueThree' + ), + 'subTwo' => (object) array( + 'propertyFour' => 'valueFour', + 'subThree' => (object) array( + 'propertyFive' => 'valueFive', + 'propertySix' => 'valueSix' + ) + ), + 'propertySeven' => 'valueSeven' + ); + } + + public function testCreate() + { + $i = new ObjectIterator($this->testObject); + + $this->assertInstanceOf('\JsonSchema\Iterator\ObjectIterator', $i); + } + + public function testInitialState() + { + $i = new ObjectIterator($this->testObject); + + $this->assertEquals($this->testObject, $i->current()); + } + + public function testCount() + { + $i = new ObjectIterator($this->testObject); + + $this->assertEquals(4, $i->count()); + } + + public function testKey() + { + $i = new ObjectIterator($this->testObject); + + while ($i->key() != 2) { + $i->next(); + } + + $this->assertEquals($this->testObject->subTwo->subThree, $i->current()); + } + + public function testAlwaysObjects() + { + $i= new ObjectIterator($this->testObject); + + foreach ($i as $item) { + $this->assertInstanceOf('\StdClass', $item); + } + } + + public function testReachesAllProperties() + { + $i = new ObjectIterator($this->testObject); + + $count = 0; + foreach ($i as $item) { + $count += count(get_object_vars($item)); + } + + $this->assertEquals(10, $count); + } +} diff --git a/tests/SchemaStorageTest.php b/tests/SchemaStorageTest.php index c3388bf4..294f0f95 100644 --- a/tests/SchemaStorageTest.php +++ b/tests/SchemaStorageTest.php @@ -11,6 +11,7 @@ use JsonSchema\SchemaStorage; use JsonSchema\Uri\UriRetriever; +use JsonSchema\Validator; use Prophecy\Argument; class SchemaStorageTest extends \PHPUnit_Framework_TestCase @@ -31,6 +32,15 @@ public function testResolveRef() ); } + public function testResolveTopRef() + { + $input = json_decode('{"propertyOne":"notANumber"}'); + $schema = json_decode('{"$ref":"#/definition","definition":{"properties":{"propertyOne":{"type":"number"}}}}'); + $v = new Validator(); + $v->validate($input, $schema); + $this->assertFalse($v->isValid()); + } + /** * @depends testResolveRef */ @@ -109,6 +119,17 @@ public function testUnresolvableJsonPointExceptionShouldBeThrown() $schemaStorage->resolveRef("$mainSchemaPath#/definitions/car"); } + public function testResolveRefWithNoAssociatedFileName() + { + $this->setExpectedException( + 'JsonSchema\Exception\UnresolvableJsonPointerException', + "Could not resolve fragment '#': no file is defined" + ); + + $schemaStorage = new SchemaStorage(); + $schemaStorage->resolveRef('#'); + } + /** * @return object */ @@ -254,4 +275,18 @@ private function getInvalidSchema() ) ); } + + public function testGetUriRetriever() + { + $s = new SchemaStorage(); + $s->addSchema('http://json-schema.org/draft-04/schema#'); + $this->assertInstanceOf('\JsonSchema\Uri\UriRetriever', $s->getUriRetriever()); + } + + public function testGetUriResolver() + { + $s = new SchemaStorage(); + $s->addSchema('http://json-schema.org/draft-04/schema#'); + $this->assertInstanceOf('\JsonSchema\Uri\UriResolver', $s->getUriResolver()); + } } diff --git a/tests/Uri/Retrievers/CurlTest.php b/tests/Uri/Retrievers/CurlTest.php new file mode 100644 index 00000000..f833b590 --- /dev/null +++ b/tests/Uri/Retrievers/CurlTest.php @@ -0,0 +1,57 @@ +retrieve(realpath(__DIR__ . '/../../fixtures/foobar.json')); + } + + public function testRetrieveNonexistantFile() + { + $c = new Curl(); + + $this->setExpectedException( + '\JsonSchema\Exception\ResourceNotFoundException', + 'JSON schema not found' + ); + $c->retrieve(__DIR__ . '/notARealFile'); + } + + public function testNoContentType() + { + $c = new Curl(); + $c->retrieve(realpath(__DIR__ . '/../../fixtures') . '/foobar-noheader.json'); + } + } +} + +namespace JsonSchema\Uri\Retrievers +{ + function curl_exec($curl) + { + $uri = curl_getinfo($curl, \CURLINFO_EFFECTIVE_URL); + + if ($uri === realpath(__DIR__ . '/../../fixtures/foobar.json')) { + // return file with headers + $headers = implode("\n", array( + 'Content-Type: application/json' + )); + + return sprintf("%s\r\n\r\n%s", $headers, file_get_contents($uri)); + } elseif ($uri === realpath(__DIR__ . '/../../fixtures') . '/foobar-noheader.json') { + // return file without headers + $uri = realpath(__DIR__ . '/../../fixtures/foobar.json'); + + return "\r\n\r\n" . file_get_contents($uri); + } + + // fallback to real curl_exec + return \curl_exec($curl); + } +} diff --git a/tests/Uri/Retrievers/FileGetContentsTest.php b/tests/Uri/Retrievers/FileGetContentsTest.php index 7b67facb..d9b06263 100644 --- a/tests/Uri/Retrievers/FileGetContentsTest.php +++ b/tests/Uri/Retrievers/FileGetContentsTest.php @@ -1,27 +1,74 @@ retrieve(__DIR__ . '/Fixture/missing.json'); + /** + * @expectedException \JsonSchema\Exception\ResourceNotFoundException + */ + public function testFetchMissingFile() + { + $res = new FileGetContents(); + $res->retrieve(__DIR__ . '/Fixture/missing.json'); + } + + public function testFetchFile() + { + $res = new FileGetContents(); + $result = $res->retrieve(__DIR__ . '/../Fixture/child.json'); + $this->assertNotEmpty($result); + } + + public function testFalseReturn() + { + $res = new FileGetContents(); + + $this->setExpectedException( + '\JsonSchema\Exception\ResourceNotFoundException', + 'JSON schema not found at http://example.com/false' + ); + $res->retrieve('http://example.com/false'); + } + + public function testFetchDirectory() + { + $res = new FileGetContents(); + + $this->setExpectedException( + '\JsonSchema\Exception\ResourceNotFoundException', + 'JSON schema not found at file:///this/is/a/directory/' + ); + $res->retrieve('file:///this/is/a/directory/'); + } + + public function testContentType() + { + $res = new FileGetContents(); + + $reflector = new \ReflectionObject($res); + $fetchContentType = $reflector->getMethod('fetchContentType'); + $fetchContentType->setAccessible(true); + + $this->assertTrue($fetchContentType->invoke($res, array('Content-Type: application/json'))); + $this->assertFalse($fetchContentType->invoke($res, array('X-Some-Header: whateverValue'))); + } } +} - public function testFetchFile() +namespace JsonSchema\Uri\Retrievers +{ + function file_get_contents($uri) { - $res = new FileGetContents(); - $result = $res->retrieve(__DIR__ . '/../Fixture/child.json'); - $this->assertNotEmpty($result); + switch ($uri) { + case 'http://example.com/false': return false; + case 'file:///this/is/a/directory/': return ''; + default: return \file_get_contents($uri); + } } } diff --git a/tests/Uri/UriRetrieverTest.php b/tests/Uri/UriRetrieverTest.php index 5d0d0e95..f5db5ca1 100644 --- a/tests/Uri/UriRetrieverTest.php +++ b/tests/Uri/UriRetrieverTest.php @@ -10,6 +10,7 @@ namespace JsonSchema\Tests\Uri; use JsonSchema\Exception\JsonDecodingException; +use JsonSchema\Uri\UriRetriever; use JsonSchema\Validator; /** @@ -279,4 +280,136 @@ private function mockRetriever($schema) $retriever->setAccessible(true); $retriever->setValue($factory, $retrieverMock); } + + public function testTranslations() + { + $retriever = new UriRetriever(); + + $uri = 'http://example.com/foo/bar'; + $translated = 'file://another/bar'; + + $retriever->setTranslation('|^https?://example.com/foo/bar#?|', 'file://another/bar'); + $this->assertEquals($translated, $retriever->translate($uri)); + } + + public function testPackageURITranslation() + { + $retriever = new UriRetriever(); + $root = sprintf('file://%s/', realpath(__DIR__ . '/../..')); + + $uri = $retriever->translate('package://foo/bar.json'); + $this->assertEquals("${root}foo/bar.json", $uri); + } + + public function testDefaultDistTranslations() + { + $retriever = new UriRetriever(); + $root = sprintf('file://%s/dist/schema/', realpath(__DIR__ . '/../..')); + + $this->assertEquals( + $root . 'json-schema-draft-03.json', + $retriever->translate('http://json-schema.org/draft-03/schema#') + ); + + $this->assertEquals( + $root . 'json-schema-draft-04.json', + $retriever->translate('http://json-schema.org/draft-04/schema#') + ); + } + + public function testRetrieveSchemaFromPackage() + { + $retriever = new UriRetriever(); + + // load schema from package + $schema = $retriever->retrieve('package://tests/fixtures/foobar.json'); + $this->assertNotFalse($schema); + + // check that the schema was loaded & processed correctly + $this->assertEquals('454f423bd7edddf0bc77af4130ed9161', md5(json_encode($schema))); + } + + public function testJsonSchemaOrgMediaTypeHack() + { + $mock = $this->getMock('JsonSchema\Uri\UriRetriever', array('getContentType')); + $mock->method('getContentType')->willReturn('Application/X-Fake-Type'); + $retriever = new UriRetriever(); + + $this->assertTrue($retriever->confirmMediaType($mock, 'http://json-schema.org/')); + } + + public function testSchemaCache() + { + $retriever = new UriRetriever(); + $reflector = new \ReflectionObject($retriever); + + // inject a schema cache value + $schemaCache = $reflector->getProperty('schemaCache'); + $schemaCache->setAccessible(true); + $schemaCache->setValue($retriever, array('local://test/uri' => 'testSchemaValue')); + + // retrieve from schema cache + $loadSchema = $reflector->getMethod('loadSchema'); + $loadSchema->setAccessible(true); + $this->assertEquals( + 'testSchemaValue', + $loadSchema->invoke($retriever, 'local://test/uri') + ); + } + + public function testLoadSchemaJSONDecodingException() + { + $retriever = new UriRetriever(); + + $this->setExpectedException( + 'JsonSchema\Exception\JsonDecodingException', + 'JSON syntax is malformed' + ); + $schema = $retriever->retrieve('package://tests/fixtures/bad-syntax.json'); + } + + public function testGenerateURI() + { + $retriever = new UriRetriever(); + $components = array( + 'scheme' => 'scheme', + 'authority' => 'authority', + 'path' => '/path', + 'query' => '?query', + 'fragment' => '#fragment' + ); + $this->assertEquals('scheme://authority/path?query#fragment', $retriever->generate($components)); + } + + public function testResolveHTTP() + { + $retriever = new UriRetriever(); + $this->assertEquals( + 'http://example.com/schema', + $retriever->resolve('http://example.com/schema') + ); + } + + public function combinedURITests() + { + return array( + array('blue', 'http://example.com/red', 'http://example.com/blue'), + array('blue', 'http://example.com/', 'http://example.com/blue'), + ); + } + + /** + * @dataProvider combinedURITests + */ + public function testResolveCombinedURI($uri, $baseURI, $combinedURI) + { + $retriever = new UriRetriever(); + $this->assertEquals($combinedURI, $retriever->resolve($uri, $baseURI)); + } + + public function testIsValidURI() + { + $retriever = new UriRetriever(); + $this->assertTrue($retriever->isValid('http://example.com/schema')); + } } diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php new file mode 100644 index 00000000..73688537 --- /dev/null +++ b/tests/ValidatorTest.php @@ -0,0 +1,59 @@ +validate($data, $schema); + + $this->assertFalse($validator->isValid(), 'Validation succeeded, but should have failed.'); + } + + public function testBadAssocSchemaInput() + { + if (version_compare(phpversion(), '5.5.0', '<')) { + $this->markTestSkipped('PHP versions < 5.5.0 trigger an error on json_encode issues'); + } + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('HHVM has no problem with encoding resources'); + } + $schema = array('propertyOne' => fopen('php://stdout', 'w')); + $data = json_decode('{"propertyOne":[42]}', true); + + $validator = new Validator(); + + $this->setExpectedException('\JsonSchema\Exception\InvalidArgumentException'); + $validator->validate($data, $schema); + } + + public function testCheck() + { + $schema = json_decode('{"type":"string"}'); + $data = json_decode('42'); + + $validator = new Validator(); + $validator->check($data, $schema); + + $this->assertFalse($validator->isValid(), 'Validation succeeded, but should have failed.'); + } + + public function testCoerce() + { + $schema = json_decode('{"type":"integer"}'); + $data = json_decode('"42"'); + + $validator = new Validator(); + $validator->coerce($data, $schema); + + $this->assertTrue($validator->isValid(), 'Validation failed, but should have succeeded.'); + } +} diff --git a/tests/fixtures/bad-syntax.json b/tests/fixtures/bad-syntax.json new file mode 100644 index 00000000..98232c64 --- /dev/null +++ b/tests/fixtures/bad-syntax.json @@ -0,0 +1 @@ +{ diff --git a/tests/fixtures/foobar.json b/tests/fixtures/foobar.json new file mode 100644 index 00000000..b27b6861 --- /dev/null +++ b/tests/fixtures/foobar.json @@ -0,0 +1,12 @@ +{ + "$id": "http://example.com/foo/bar#", + "type": "object", + "properties": { + "foo": { + "type": "string", + "default": "bar" + } + }, + "required": ["foo"], + "additionalProperties": false +} diff --git a/tests/fixtures/json-schema-draft-03.json b/tests/fixtures/json-schema-draft-03.json deleted file mode 100644 index dcf07342..00000000 --- a/tests/fixtures/json-schema-draft-03.json +++ /dev/null @@ -1,193 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-03/schema#", - "id": "http://json-schema.org/draft-03/schema#", - "type": "object", - "properties": { - "type": { - "type": [ - "string", - "array" - ], - "items": { - "type": [ - "string", - { - "$ref": "#" - } - ] - }, - "uniqueItems": true, - "default": "any" - }, - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "additionalProperties": { - "type": [ - { - "$ref": "#" - }, - "boolean" - ], - "default": {} - }, - "items": { - "type": [ - { - "$ref": "#" - }, - "array" - ], - "items": { - "$ref": "#" - }, - "default": {} - }, - "additionalItems": { - "type": [ - { - "$ref": "#" - }, - "boolean" - ], - "default": {} - }, - "required": { - "type": "boolean", - "default": false - }, - "dependencies": { - "type": "object", - "additionalProperties": { - "type": [ - "string", - "array", - { - "$ref": "#" - } - ], - "items": { - "type": "string" - } - }, - "default": {} - }, - "minimum": { - "type": "number" - }, - "maximum": { - "type": "number" - }, - "exclusiveMinimum": { - "type": "boolean", - "default": false - }, - "exclusiveMaximum": { - "type": "boolean", - "default": false - }, - "minItems": { - "type": "integer", - "minimum": 0, - "default": 0 - }, - "maxItems": { - "type": "integer", - "minimum": 0 - }, - "uniqueItems": { - "type": "boolean", - "default": false - }, - "pattern": { - "type": "string", - "format": "regex" - }, - "minLength": { - "type": "integer", - "minimum": 0, - "default": 0 - }, - "maxLength": { - "type": "integer" - }, - "enum": { - "type": "array", - "minItems": 1, - "uniqueItems": true - }, - "default": { - "type": "any" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "format": { - "type": "string" - }, - "divisibleBy": { - "type": "number", - "minimum": 0, - "exclusiveMinimum": true, - "default": 1 - }, - "disallow": { - "type": [ - "string", - "array" - ], - "items": { - "type": [ - "string", - { - "$ref": "#" - } - ] - }, - "uniqueItems": true - }, - "extends": { - "type": [ - { - "$ref": "#" - }, - "array" - ], - "items": { - "$ref": "#" - }, - "default": {} - }, - "id": { - "type": "string", - "format": "uri" - }, - "$ref": { - "type": "string", - "format": "uri" - }, - "$schema": { - "type": "string", - "format": "uri" - } - }, - "dependencies": { - "exclusiveMinimum": "minimum", - "exclusiveMaximum": "maximum" - }, - "default": {} -} \ No newline at end of file diff --git a/tests/fixtures/json-schema-draft-04.json b/tests/fixtures/json-schema-draft-04.json deleted file mode 100644 index 96e7f16a..00000000 --- a/tests/fixtures/json-schema-draft-04.json +++ /dev/null @@ -1,221 +0,0 @@ -{ - "id": "http://json-schema.org/draft-04/schema#", - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Core schema meta-schema", - "definitions": { - "schemaArray": { - "type": "array", - "minItems": 1, - "items": { - "$ref": "#" - } - }, - "positiveInteger": { - "type": "integer", - "minimum": 0 - }, - "positiveIntegerDefault0": { - "allOf": [ - { - "$ref": "#/definitions/positiveInteger" - }, - { - "default": 0 - } - ] - }, - "simpleTypes": { - "enum": [ - "array", - "boolean", - "integer", - "null", - "number", - "object", - "string" - ] - }, - "stringArray": { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1, - "uniqueItems": true - } - }, - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uri" - }, - "$schema": { - "type": "string", - "format": "uri" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "default": {}, - "multipleOf": { - "type": "number", - "minimum": 0, - "exclusiveMinimum": true - }, - "maximum": { - "type": "number" - }, - "exclusiveMaximum": { - "type": "boolean", - "default": false - }, - "minimum": { - "type": "number" - }, - "exclusiveMinimum": { - "type": "boolean", - "default": false - }, - "maxLength": { - "$ref": "#/definitions/positiveInteger" - }, - "minLength": { - "$ref": "#/definitions/positiveIntegerDefault0" - }, - "pattern": { - "type": "string", - "format": "regex" - }, - "additionalItems": { - "anyOf": [ - { - "type": "boolean" - }, - { - "$ref": "#" - } - ], - "default": {} - }, - "items": { - "anyOf": [ - { - "$ref": "#" - }, - { - "$ref": "#/definitions/schemaArray" - } - ], - "default": {} - }, - "maxItems": { - "$ref": "#/definitions/positiveInteger" - }, - "minItems": { - "$ref": "#/definitions/positiveIntegerDefault0" - }, - "uniqueItems": { - "type": "boolean", - "default": false - }, - "maxProperties": { - "$ref": "#/definitions/positiveInteger" - }, - "minProperties": { - "$ref": "#/definitions/positiveIntegerDefault0" - }, - "required": { - "$ref": "#/definitions/stringArray" - }, - "additionalProperties": { - "anyOf": [ - { - "type": "boolean" - }, - { - "$ref": "#" - } - ], - "default": {} - }, - "definitions": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "properties": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "patternProperties": { - "type": "object", - "additionalProperties": { - "$ref": "#" - }, - "default": {} - }, - "dependencies": { - "type": "object", - "additionalProperties": { - "anyOf": [ - { - "$ref": "#" - }, - { - "$ref": "#/definitions/stringArray" - } - ] - } - }, - "enum": { - "type": "array", - "minItems": 1, - "uniqueItems": true - }, - "type": { - "anyOf": [ - { - "$ref": "#/definitions/simpleTypes" - }, - { - "type": "array", - "items": { - "$ref": "#/definitions/simpleTypes" - }, - "minItems": 1, - "uniqueItems": true - } - ] - }, - "allOf": { - "$ref": "#/definitions/schemaArray" - }, - "anyOf": { - "$ref": "#/definitions/schemaArray" - }, - "oneOf": { - "$ref": "#/definitions/schemaArray" - }, - "not": { - "$ref": "#" - } - }, - "dependencies": { - "exclusiveMaximum": [ - "maximum" - ], - "exclusiveMinimum": [ - "minimum" - ] - }, - "default": {} -} \ No newline at end of file