From 93c5a64d6dc6109743988c0e4d7fe1211fc90ff8 Mon Sep 17 00:00:00 2001 From: Erayd Date: Wed, 1 Mar 2017 17:30:26 +1300 Subject: [PATCH 01/14] Don't try to fetch files that don't exist Throws an exception when the ref can't be resolved to a useful file URI, rather than waiting for something further down the line to fail after the fact. --- src/JsonSchema/SchemaStorage.php | 12 +++++++++++- tests/SchemaStorageTest.php | 11 +++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/JsonSchema/SchemaStorage.php b/src/JsonSchema/SchemaStorage.php index 55d7985a..3ee081e3 100644 --- a/src/JsonSchema/SchemaStorage.php +++ b/src/JsonSchema/SchemaStorage.php @@ -79,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/tests/SchemaStorageTest.php b/tests/SchemaStorageTest.php index cf20ff7b..294f0f95 100644 --- a/tests/SchemaStorageTest.php +++ b/tests/SchemaStorageTest.php @@ -119,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 */ From 976cfb0cfbc6c1988d7fcddc7aadbd5ce819a643 Mon Sep 17 00:00:00 2001 From: Erayd Date: Thu, 2 Mar 2017 00:33:55 +1300 Subject: [PATCH 02/14] Refactor defaults code to use LooseTypeCheck where appropriate --- .../Constraints/UndefinedConstraint.php | 38 ++++++------------- tests/Constraints/DefaultPropertiesTest.php | 11 ++++++ 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 264a8d36..6a804ad0 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -113,38 +113,24 @@ 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 + if (isset($schema->properties) && LooseTypeCheck::isObject($value)) { foreach ($schema->properties as $currentProperty => $propertyDefinition) { - if (!$this->getTypeCheck()->propertyExists($value, $currentProperty) && isset($propertyDefinition->default)) { + if (!LooseTypeCheck::propertyExists($value, $currentProperty) && isset($propertyDefinition->default)) { if (is_object($propertyDefinition->default)) { - $this->getTypeCheck()->propertySet($value, $currentProperty, clone $propertyDefinition->default); + LooseTypeCheck::propertySet($value, $currentProperty, clone $propertyDefinition->default); } else { - $this->getTypeCheck()->propertySet($value, $currentProperty, $propertyDefinition->default); + LooseTypeCheck::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 (isset($schema->items) && LooseTypeCheck::isArray($value)) { + // $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; } } } diff --git a/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php index 6687e7c2..75f1d3f7 100644 --- a/tests/Constraints/DefaultPropertiesTest.php +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -150,4 +150,15 @@ public function testNoModificationViaReferences() $input->propertyOne = 'valueTwo'; $this->assertEquals('valueOne', $schema->default->propertyOne); } + + 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)); + } } From bdf8d060cc7da36d93db3d7db0805b3d402a9175 Mon Sep 17 00:00:00 2001 From: Erayd Date: Thu, 2 Mar 2017 00:38:25 +1300 Subject: [PATCH 03/14] Test for not treating non-containers like arrays --- tests/Constraints/DefaultPropertiesTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php index 75f1d3f7..f1c1dc24 100644 --- a/tests/Constraints/DefaultPropertiesTest.php +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -160,5 +160,8 @@ public function testLeaveBasicTypesAlone() $validator->validate($input, $schema, Constraint::CHECK_MODE_APPLY_DEFAULTS); $this->assertEquals('"ThisIsAString"', json_encode($input)); + + $schema = json_decode('{"items":[{"type":"string","default":"valueOne"}]}'); + $this->assertEquals('"ThisIsAString"', json_encode($input)); } } From 8ecbca23832b4c5200929076d3d2bc9a215e6348 Mon Sep 17 00:00:00 2001 From: Erayd Date: Thu, 2 Mar 2017 00:48:08 +1300 Subject: [PATCH 04/14] Update comments --- src/JsonSchema/Constraints/UndefinedConstraint.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 6a804ad0..a3e96af5 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -114,6 +114,7 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer // Apply default values from schema if ($this->factory->getConfig(self::CHECK_MODE_APPLY_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) && isset($propertyDefinition->default)) { if (is_object($propertyDefinition->default)) { @@ -124,7 +125,7 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer } } } elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) { - // $value is an array, and default items are defined - treat as plain array + // $value is an array, and 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)) { From 7b869c0c9f87f8d1478ab7b881d7f9e83e9ded79 Mon Sep 17 00:00:00 2001 From: Erayd Date: Thu, 2 Mar 2017 01:51:54 +1300 Subject: [PATCH 05/14] Rename variable for clarity --- src/JsonSchema/Constraints/UndefinedConstraint.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index a3e96af5..1b406e2a 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -126,12 +126,12 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer } } elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) { // $value is an array, and items are defined - treat as plain array - foreach ($schema->items as $currentProperty => $itemDefinition) { - if (!isset($value[$currentProperty]) && isset($itemDefinition->default)) { + foreach ($schema->items as $currentItem => $itemDefinition) { + if (!isset($value[$currentItem]) && isset($itemDefinition->default)) { if (is_object($itemDefinition->default)) { - $value[$currentProperty] = clone $itemDefinition->default; + $value[$currentItem] = clone $itemDefinition->default; } else { - $value[$currentProperty] = $itemDefinition->default; + $value[$currentItem] = $itemDefinition->default; } } } From 4ed0591dbe89795b0d4b7beb1869c541449561aa Mon Sep 17 00:00:00 2001 From: Erayd Date: Tue, 7 Mar 2017 10:21:05 +1300 Subject: [PATCH 06/14] Add CHECK_MODE_ONLY_REQUIRED_DEFAULTS If CHECK_MODE_ONLY_REQUIRED_DEFAULTS is set, then only apply defaults if they are marked as required. --- README.md | 1 + src/JsonSchema/Constraints/Constraint.php | 1 + .../Constraints/UndefinedConstraint.php | 99 +++++++++++++------ tests/Constraints/DefaultPropertiesTest.php | 73 +++++++++----- 4 files changed, 120 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 4de425f5..46adffbe 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,7 @@ 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 | diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index 2283ac12..e073eb54 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -31,6 +31,7 @@ abstract class Constraint extends BaseConstraint implements ConstraintInterface const CHECK_MODE_APPLY_DEFAULTS = 0x00000008; const CHECK_MODE_EXCEPTIONS = 0x00000010; const CHECK_MODE_DISABLE_FORMAT = 0x00000020; + const CHECK_MODE_ONLY_REQUIRED_DEFAULTS = 0x00000080; /** * Bubble down the path diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 1b406e2a..d0adfda1 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -112,34 +112,7 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer } // Apply default values from schema - if ($this->factory->getConfig(self::CHECK_MODE_APPLY_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) && isset($propertyDefinition->default)) { - if (is_object($propertyDefinition->default)) { - LooseTypeCheck::propertySet($value, $currentProperty, clone $propertyDefinition->default); - } else { - LooseTypeCheck::propertySet($value, $currentProperty, $propertyDefinition->default); - } - } - } - } 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 (!isset($value[$currentItem]) && isset($itemDefinition->default)) { - if (is_object($itemDefinition->default)) { - $value[$currentItem] = clone $itemDefinition->default; - } else { - $value[$currentItem] = $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; - } - } + $this->applyDefaultValues($value, $schema); // Verify required values if ($this->getTypeCheck()->isObject($value)) { @@ -202,6 +175,76 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer } } + /** + * Apply default values + * + * @param mixed $value + * @param mixed $schema + */ + protected function applyDefaultValues(&$value, $schema) + { + // only apply defaults if feature is enabled + if (!$this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) { + return; + } + + // check whether this default should be applied + $shouldApply = function ($definition, $name = null) use ($schema) { + // required-only mode is off + if (!$this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_DEFAULTS)) { + return true; + } + // draft-04 required is set + if ( + $name !== null + && isset($schema->required) + && is_array($schema->required) + && in_array($name, $schema->required) + ) { + return true; + } + // draft-03 required is set + if (isset($definition->required) && !is_array($definition->required) && $definition->required) { + return true; + } + // default case + return false; + }; + + // apply defaults if appropriate + 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) + && isset($propertyDefinition->default) + && $shouldApply($propertyDefinition, $currentProperty) + ) { + // assign default value + if (is_object($propertyDefinition->default)) { + LooseTypeCheck::propertySet($value, $currentProperty, clone $propertyDefinition->default); + } else { + LooseTypeCheck::propertySet($value, $currentProperty, $propertyDefinition->default); + } + } + } + } 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 (!isset($value[$currentItem]) && isset($itemDefinition->default) && $shouldApply($itemDefinition)) { + if (is_object($itemDefinition->default)) { + $value[$currentItem] = clone $itemDefinition->default; + } else { + $value[$currentItem] = $itemDefinition->default; + } + } + } + } elseif (($value instanceof self || $value === null) && isset($schema->default) && $shouldApply($schema)) { + // $value is a leaf, not a container - apply the default directly + $value = is_object($schema->default) ? clone $schema->default : $schema->default; + } + } + /** * Validate allOf, anyOf, and oneOf properties * diff --git a/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php index f1c1dc24..6feb0b55 100644 --- a/tests/Constraints/DefaultPropertiesTest.php +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -19,75 +19,98 @@ class DefaultPropertiesTest extends VeryBaseTestCase public function getValidTests() { return array( - array(// default value for entire object + array(// #0 default value for entire object '', '{"default":"valueOne"}', '"valueOne"' ), - array(// default value in an empty object + array(// #1 default value in an empty object '{}', '{"properties":{"propertyOne":{"default":"valueOne"}}}', '{"propertyOne":"valueOne"}' ), - array(// default value for top-level property + array(// #2 default value for top-level property '{"propertyOne":"valueOne"}', '{"properties":{"propertyTwo":{"default":"valueTwo"}}}', '{"propertyOne":"valueOne","propertyTwo":"valueTwo"}' ), - array(// default value for sub-property + array(// #3 default value for sub-property '{"propertyOne":{}}', '{"properties":{"propertyOne":{"properties":{"propertyTwo":{"default":"valueTwo"}}}}}', '{"propertyOne":{"propertyTwo":"valueTwo"}}' ), - array(// default value for sub-property with sibling + array(// #4 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(// #5 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(// #6 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(// #7 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(// #8 default value for an already set property '{"propertyOne":"alreadySetValueOne"}', '{"properties":{"propertyOne":{"default":"valueOne"}}}', '{"propertyOne":"alreadySetValueOne"}' ), - array(//default item value for an array + array(// #9 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(// #10 default item value for an empty array '[]', '{"type":"array","items":[{"type":"string","default":"valueOne"}]}', '["valueOne"]' ), - array(//property without a default available + array(// #11 property without a default available '{"propertyOne":"alreadySetValueOne"}', '{"properties":{"propertyOne":{"type":"string"}}}', '{"propertyOne":"alreadySetValueOne"}' ), - array(// default property value is an object + array(// #12 default property value is an object '{"propertyOne":"valueOne"}', '{"properties":{"propertyTwo":{"default":{}}}}', '{"propertyOne":"valueOne","propertyTwo":{}}' ), - array(// default item value is an object + array(// #13 default item value is an object '[]', '{"type":"array","items":[{"default":{}}]}', '[{}]' + ), + array(// #14 only set required values (draft-04) + '{}', + '{ + "properties": { + "propertyOne": {"default": "valueOne"}, + "propertyTwo": {"default": "valueTwo"} + }, + "required": ["propertyTwo"] + }', + '{"propertyTwo":"valueTwo"}', + Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS + ), + array(// #15 only set required values (draft-03) + '{}', + '{ + "properties": { + "propertyOne": {"default": "valueOne"}, + "propertyTwo": {"default": "valueTwo", "required": true} + } + }', + '{"propertyTwo":"valueTwo"}', + Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS ) ); } @@ -95,7 +118,7 @@ public function getValidTests() /** * @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 +126,9 @@ 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; + $validator = new Validator(); + $validator->validate($inputDecoded, json_decode($schema), $checkMode); $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); @@ -119,22 +140,22 @@ 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() From 35c03bbe4617332111df2e3bd896a18a6ec1bc43 Mon Sep 17 00:00:00 2001 From: Erayd Date: Tue, 7 Mar 2017 10:31:08 +1300 Subject: [PATCH 07/14] Workaround for $this scope issue on PHP-5.3 --- src/JsonSchema/Constraints/UndefinedConstraint.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index d0adfda1..99ad2b0d 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -189,9 +189,10 @@ protected function applyDefaultValues(&$value, $schema) } // check whether this default should be applied - $shouldApply = function ($definition, $name = null) use ($schema) { + $requiredOnly = $this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_DEFAULTS); + $shouldApply = function ($definition, $name = null) use ($schema, $requiredOnly) { // required-only mode is off - if (!$this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_DEFAULTS)) { + if (!$requiredOnly) { return true; } // draft-04 required is set From 50c26d062d8e9047b6d6f880ce9a5256e80c665b Mon Sep 17 00:00:00 2001 From: Erayd Date: Tue, 7 Mar 2017 12:42:04 +1300 Subject: [PATCH 08/14] Fix infinite recursion via $ref when applying defaults --- src/JsonSchema/Constraints/Constraint.php | 8 +++---- .../Constraints/ObjectConstraint.php | 17 ++++++++++---- .../Constraints/UndefinedConstraint.php | 18 ++++++++++++--- src/JsonSchema/Entity/JsonPointer.php | 23 +++++++++++++++++++ tests/Constraints/DefaultPropertiesTest.php | 21 +++++++++++++++-- 5 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/JsonSchema/Constraints/Constraint.php b/src/JsonSchema/Constraints/Constraint.php index e073eb54..28c8d44c 100644 --- a/src/JsonSchema/Constraints/Constraint.php +++ b/src/JsonSchema/Constraints/Constraint.php @@ -79,10 +79,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()); } @@ -111,11 +111,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/ObjectConstraint.php b/src/JsonSchema/Constraints/ObjectConstraint.php index d360a659..3c345619 100644 --- a/src/JsonSchema/Constraints/ObjectConstraint.php +++ b/src/JsonSchema/Constraints/ObjectConstraint.php @@ -20,15 +20,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); @@ -64,7 +71,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)); } } } @@ -96,9 +103,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)); } } @@ -135,7 +142,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/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 99ad2b0d..3e166812 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -22,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); @@ -67,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 ); } @@ -112,7 +121,9 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer } // Apply default values from schema - $this->applyDefaultValues($value, $schema); + if (!$path->fromDefault()) { + $this->applyDefaultValues($value, $schema); + } // Verify required values if ($this->getTypeCheck()->isObject($value)) { @@ -227,6 +238,7 @@ protected function applyDefaultValues(&$value, $schema) } else { LooseTypeCheck::propertySet($value, $currentProperty, $propertyDefinition->default); } + $this->appliedDefaults[] = $currentProperty; } } } elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) { diff --git a/src/JsonSchema/Entity/JsonPointer.php b/src/JsonSchema/Entity/JsonPointer.php index 8bb71ecf..fcaf5b8d 100644 --- a/src/JsonSchema/Entity/JsonPointer.php +++ b/src/JsonSchema/Entity/JsonPointer.php @@ -24,6 +24,11 @@ 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 * @@ -135,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/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php index 6feb0b55..63523ce5 100644 --- a/tests/Constraints/DefaultPropertiesTest.php +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -111,6 +111,18 @@ public function getValidTests() }', '{"propertyTwo":"valueTwo"}', Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS + ), + array(// #16 infinite recursion via $ref + '{}', + '{ + "properties": { + "propertyOne": { + "$ref": "#", + "default": {} + } + } + }', + '{"propertyOne":{}}' ) ); } @@ -127,8 +139,13 @@ public function testValidCases($input, $schema, $expectOutput = null, $checkMode } $checkMode |= Constraint::CHECK_MODE_APPLY_DEFAULTS; - $validator = new Validator(); - $validator->validate($inputDecoded, json_decode($schema), $checkMode); + + $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)); From 852ad07dd262b7f5b6db2de7fbf2b783d4f2b6d5 Mon Sep 17 00:00:00 2001 From: Erayd Date: Tue, 7 Mar 2017 13:20:59 +1300 Subject: [PATCH 09/14] Add missing second test for array case --- tests/Constraints/DefaultPropertiesTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php index 63523ce5..ec6479fc 100644 --- a/tests/Constraints/DefaultPropertiesTest.php +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -200,6 +200,7 @@ public function testLeaveBasicTypesAlone() $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)); } } From 37aa991dab5300cd5dee10a0760a7a257a78eca5 Mon Sep 17 00:00:00 2001 From: Erayd Date: Tue, 7 Mar 2017 13:21:32 +1300 Subject: [PATCH 10/14] Add test for setting a default value for null --- tests/Constraints/DefaultPropertiesTest.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php index ec6479fc..fa0cf3a8 100644 --- a/tests/Constraints/DefaultPropertiesTest.php +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -114,15 +114,13 @@ public function getValidTests() ), array(// #16 infinite recursion via $ref '{}', - '{ - "properties": { - "propertyOne": { - "$ref": "#", - "default": {} - } - } - }', + '{"properties":{"propertyOne": {"$ref": "#","default": {}}}}', '{"propertyOne":{}}' + ), + array(// #17 default value for null + 'null', + '{"default":"valueOne"}', + '"valueOne"' ) ); } From e0baac812c1eb8c47db949ce712fce6da099ad6b Mon Sep 17 00:00:00 2001 From: Erayd Date: Tue, 7 Mar 2017 13:41:03 +1300 Subject: [PATCH 11/14] Also fix infinite recursion via $ref for array defaults --- src/JsonSchema/Constraints/UndefinedConstraint.php | 11 +++++++---- tests/Constraints/DefaultPropertiesTest.php | 9 +++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 3e166812..e3230441 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -122,7 +122,7 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer // Apply default values from schema if (!$path->fromDefault()) { - $this->applyDefaultValues($value, $schema); + $this->applyDefaultValues($value, $schema, $path); } // Verify required values @@ -189,10 +189,11 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer /** * Apply default values * - * @param mixed $value - * @param mixed $schema + * @param mixed $value + * @param mixed $schema + * @param JsonPointer $path */ - protected function applyDefaultValues(&$value, $schema) + protected function applyDefaultValues(&$value, $schema, $path) { // only apply defaults if feature is enabled if (!$this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) { @@ -251,10 +252,12 @@ protected function applyDefaultValues(&$value, $schema) $value[$currentItem] = $itemDefinition->default; } } + $path->setFromDefault(); } } elseif (($value instanceof self || $value === null) && isset($schema->default) && $shouldApply($schema)) { // $value is a leaf, not a container - apply the default directly $value = is_object($schema->default) ? clone $schema->default : $schema->default; + $path->setFromDefault(); } } diff --git a/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php index fa0cf3a8..508e284b 100644 --- a/tests/Constraints/DefaultPropertiesTest.php +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -112,12 +112,17 @@ public function getValidTests() '{"propertyTwo":"valueTwo"}', Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS ), - array(// #16 infinite recursion via $ref + array(// #16 infinite recursion via $ref (object) '{}', '{"properties":{"propertyOne": {"$ref": "#","default": {}}}}', '{"propertyOne":{}}' ), - array(// #17 default value for null + array(// #17 infinite recursion via $ref (array) + '[]', + '{"items":[{"$ref":"#","default":[]}]}', + '[[]]' + ), + array(// #18 default value for null 'null', '{"default":"valueOne"}', '"valueOne"' From e098ee7dccfd71f1858658319bdc288559e3c667 Mon Sep 17 00:00:00 2001 From: Erayd Date: Tue, 7 Mar 2017 13:58:35 +1300 Subject: [PATCH 12/14] Move nested closure into separate method --- .../Constraints/UndefinedConstraint.php | 70 ++++++++++++------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index e3230441..099b7f17 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -186,6 +186,39 @@ 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 + && is_object($parentSchema) + && 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 * @@ -200,38 +233,15 @@ protected function applyDefaultValues(&$value, $schema, $path) return; } - // check whether this default should be applied - $requiredOnly = $this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_DEFAULTS); - $shouldApply = function ($definition, $name = null) use ($schema, $requiredOnly) { - // required-only mode is off - if (!$requiredOnly) { - return true; - } - // draft-04 required is set - if ( - $name !== null - && isset($schema->required) - && is_array($schema->required) - && in_array($name, $schema->required) - ) { - return true; - } - // draft-03 required is set - if (isset($definition->required) && !is_array($definition->required) && $definition->required) { - return true; - } - // default case - return false; - }; - // 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) && isset($propertyDefinition->default) - && $shouldApply($propertyDefinition, $currentProperty) + && $this->shouldApplyDefaultValue($requiredOnly, $propertyDefinition, $currentProperty, $schema) ) { // assign default value if (is_object($propertyDefinition->default)) { @@ -245,7 +255,10 @@ protected function applyDefaultValues(&$value, $schema, $path) } 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 (!isset($value[$currentItem]) && isset($itemDefinition->default) && $shouldApply($itemDefinition)) { + if ( + !isset($value[$currentItem]) + && isset($itemDefinition->default) + && $this->shouldApplyDefaultValue($requiredOnly, $itemDefinition)) { if (is_object($itemDefinition->default)) { $value[$currentItem] = clone $itemDefinition->default; } else { @@ -254,7 +267,10 @@ protected function applyDefaultValues(&$value, $schema, $path) } $path->setFromDefault(); } - } elseif (($value instanceof self || $value === null) && isset($schema->default) && $shouldApply($schema)) { + } elseif ( + ($value instanceof self || $value === null) + && isset($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(); From c4031e15d9b64d9d8d3385458418d758e16efa2c Mon Sep 17 00:00:00 2001 From: Erayd Date: Tue, 7 Mar 2017 14:02:18 +1300 Subject: [PATCH 13/14] $parentSchema will always be set when $name is, so don't check it --- src/JsonSchema/Constraints/UndefinedConstraint.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 099b7f17..67afbe60 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -204,7 +204,6 @@ private function shouldApplyDefaultValue($requiredOnly, $schema, $name = null, $ // draft-04 required is set if ( $name !== null - && is_object($parentSchema) && isset($parentSchema->required) && is_array($parentSchema->required) && in_array($name, $parentSchema->required) From f63d9386315f4363cd53b3ab950a45c3c69b8db5 Mon Sep 17 00:00:00 2001 From: Erayd Date: Sat, 11 Mar 2017 06:18:38 +1300 Subject: [PATCH 14/14] Handle nulls properly - fixes issue #377 --- .../Constraints/UndefinedConstraint.php | 10 +-- tests/Constraints/DefaultPropertiesTest.php | 68 ++++++++++++------- 2 files changed, 50 insertions(+), 28 deletions(-) diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 67afbe60..ba624566 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -239,7 +239,7 @@ protected function applyDefaultValues(&$value, $schema, $path) foreach ($schema->properties as $currentProperty => $propertyDefinition) { if ( !LooseTypeCheck::propertyExists($value, $currentProperty) - && isset($propertyDefinition->default) + && property_exists($propertyDefinition, 'default') && $this->shouldApplyDefaultValue($requiredOnly, $propertyDefinition, $currentProperty, $schema) ) { // assign default value @@ -255,8 +255,8 @@ protected function applyDefaultValues(&$value, $schema, $path) // $value is an array, and items are defined - treat as plain array foreach ($schema->items as $currentItem => $itemDefinition) { if ( - !isset($value[$currentItem]) - && isset($itemDefinition->default) + !array_key_exists($currentItem, $value) + && property_exists($itemDefinition, 'default') && $this->shouldApplyDefaultValue($requiredOnly, $itemDefinition)) { if (is_object($itemDefinition->default)) { $value[$currentItem] = clone $itemDefinition->default; @@ -267,8 +267,8 @@ protected function applyDefaultValues(&$value, $schema, $path) $path->setFromDefault(); } } elseif ( - ($value instanceof self || $value === null) - && isset($schema->default) + $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; diff --git a/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php index 508e284b..a3b9c4e2 100644 --- a/tests/Constraints/DefaultPropertiesTest.php +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -19,77 +19,84 @@ class DefaultPropertiesTest extends VeryBaseTestCase public function getValidTests() { return array( + /* + // 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(// #1 default value in an empty object + */ + array(// #0 default value in an empty object '{}', '{"properties":{"propertyOne":{"default":"valueOne"}}}', '{"propertyOne":"valueOne"}' ), - array(// #2 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(// #3 default value for sub-property + array(// #2 default value for sub-property '{"propertyOne":{}}', '{"properties":{"propertyOne":{"properties":{"propertyTwo":{"default":"valueTwo"}}}}}', '{"propertyOne":{"propertyTwo":"valueTwo"}}' ), - array(// #4 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(// #5 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(// #6 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(// #7 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(// #8 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(// #9 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(// #10 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(// #11 property without a default available + array(// #10 property without a default available '{"propertyOne":"alreadySetValueOne"}', '{"properties":{"propertyOne":{"type":"string"}}}', '{"propertyOne":"alreadySetValueOne"}' ), - array(// #12 default property value is an object + array(// #11 default property value is an object '{"propertyOne":"valueOne"}', '{"properties":{"propertyTwo":{"default":{}}}}', '{"propertyOne":"valueOne","propertyTwo":{}}' ), - array(// #13 default item value is an object + array(// #12 default item value is an object '[]', '{"type":"array","items":[{"default":{}}]}', '[{}]' ), - array(// #14 only set required values (draft-04) + array(// #13 only set required values (draft-04) '{}', '{ "properties": { @@ -101,7 +108,7 @@ public function getValidTests() '{"propertyTwo":"valueTwo"}', Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS ), - array(// #15 only set required values (draft-03) + array(// #14 only set required values (draft-03) '{}', '{ "properties": { @@ -112,21 +119,36 @@ public function getValidTests() '{"propertyTwo":"valueTwo"}', Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS ), - array(// #16 infinite recursion via $ref (object) + array(// #15 infinite recursion via $ref (object) '{}', '{"properties":{"propertyOne": {"$ref": "#","default": {}}}}', '{"propertyOne":{}}' ), - array(// #17 infinite recursion via $ref (array) + array(// #16 infinite recursion via $ref (array) '[]', '{"items":[{"$ref":"#","default":[]}]}', '[[]]' ), - array(// #18 default value for null + array(// #17 default top value does not overwrite defined null 'null', '{"default":"valueOne"}', - '"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]' + ), ); } @@ -180,8 +202,8 @@ public function testValidCasesUsingAssocWithoutTypeCast($input, $schema, $expect 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); @@ -189,7 +211,7 @@ 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()