diff --git a/README.md b/README.md index fee6dfb9..e3ef0ccf 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,40 @@ $validator->coerce($request, $schema); // equivalent to $validator->validate($data, $schema, Constraint::CHECK_MODE_COERCE_TYPES); ``` +### Default values + +If your schema contains default values, you can have these automatically applied during validation: + +```php +17 +]; + +$validator = new Validator(); + +$validator->validate( + $request, + (object)[ + "type"=>"object", + "properties"=>(object)[ + "processRefund"=>(object)[ + "type"=>"boolean", + "default"=>true + ] + ] + ], + Constraint::CHECK_MODE_APPLY_DEFAULTS +); //validates, and sets defaults for missing properties + +is_bool($request->processRefund); // true +$request->processRefund; // true +``` + ### With inline references ```php @@ -152,9 +186,11 @@ third argument to `Validator::validate()`, or can be provided as the third argum | `Constraint::CHECK_MODE_NORMAL` | Validate in 'normal' mode - this is the default | | `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_EXCEPTIONS` | Throw an exception immediately if validation fails | -Please note that using `Constraint::CHECK_MODE_COERCE_TYPES` will modify your original data. +Please note that using `Constraint::CHECK_MODE_COERCE_TYPES` or `Constraint::CHECK_MODE_APPLY_DEFAULTS` +will modify your original data. ## Running the tests diff --git a/src/JsonSchema/Constraints/Factory.php b/src/JsonSchema/Constraints/Factory.php index f6b97457..8c24873f 100644 --- a/src/JsonSchema/Constraints/Factory.php +++ b/src/JsonSchema/Constraints/Factory.php @@ -9,6 +9,7 @@ namespace JsonSchema\Constraints; +use JsonSchema\Constraints\Constraint; use JsonSchema\Exception\InvalidArgumentException; use JsonSchema\Exception\InvalidConfigException; use JsonSchema\SchemaStorage; diff --git a/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php b/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php index 4818bf6d..98428853 100644 --- a/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php +++ b/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php @@ -27,6 +27,15 @@ public static function propertyGet($value, $property) return $value[$property]; } + public static function propertySet(&$value, $property, $data) + { + if (is_object($value)) { + $value->{$property} = $data; + } else { + $value[$property] = $data; + } + } + public static function propertyExists($value, $property) { if (is_object($value)) { diff --git a/src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php b/src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php index 73bbc383..a6303a7a 100644 --- a/src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php +++ b/src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php @@ -19,6 +19,11 @@ public static function propertyGet($value, $property) return $value->{$property}; } + public static function propertySet(&$value, $property, $data) + { + $value->{$property} = $data; + } + public static function propertyExists($value, $property) { return property_exists($value, $property); diff --git a/src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php b/src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php index 5fd68acd..10b40ea0 100644 --- a/src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php +++ b/src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php @@ -10,6 +10,8 @@ public static function isArray($value); public static function propertyGet($value, $property); + public static function propertySet(&$value, $property, $data); + public static function propertyExists($value, $property); public static function propertyCount($value); diff --git a/src/JsonSchema/Constraints/UndefinedConstraint.php b/src/JsonSchema/Constraints/UndefinedConstraint.php index 2b0688ec..88169306 100644 --- a/src/JsonSchema/Constraints/UndefinedConstraint.php +++ b/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -9,6 +9,7 @@ namespace JsonSchema\Constraints; +use JsonSchema\Constraints\TypeCheck\LooseTypeCheck; use JsonSchema\Entity\JsonPointer; use JsonSchema\Uri\UriResolver; @@ -57,7 +58,9 @@ public function validateTypes(&$value, $schema = null, JsonPointer $path, $i = n } // check object - if ($this->getTypeCheck()->isObject($value)) { + if (LooseTypeCheck::isObject($value)) { // object processing should always be run on assoc arrays, + // so use LooseTypeCheck here even if CHECK_MODE_TYPE_CAST + // is not set (i.e. don't use $this->getTypeCheck() here). $this->checkObject( $value, isset($schema->properties) ? $this->factory->getSchemaStorage()->resolveRefSchema($schema->properties) : $schema, @@ -107,6 +110,49 @@ 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 $i => $propertyDefinition) { + if (!$this->getTypeCheck()->propertyExists($value, $i) && isset($propertyDefinition->default)) { + if (is_object($propertyDefinition->default)) { + $this->getTypeCheck()->propertySet($value, $i, clone $propertyDefinition->default); + } else { + $this->getTypeCheck()->propertySet($value, $i, $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 $i => $propertyDefinition) { + if (!isset($value[$i]) && isset($propertyDefinition->default)) { + if (is_object($propertyDefinition->default)) { + $value[$i] = clone $propertyDefinition->default; + } else { + $value[$i] = $propertyDefinition->default; + } + } + } + } elseif (isset($schema->items)) { + // $value is an array, and default items are defined - treat as plain array + foreach ($schema->items as $i => $itemDefinition) { + if (!isset($value[$i]) && isset($itemDefinition->default)) { + if (is_object($itemDefinition->default)) { + $value[$i] = clone $itemDefinition->default; + } else { + $value[$i] = $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; + } + } + // Verify required values if ($this->getTypeCheck()->isObject($value)) { if (!($value instanceof self) && isset($schema->required) && is_array($schema->required)) { diff --git a/tests/Constraints/DefaultPropertiesTest.php b/tests/Constraints/DefaultPropertiesTest.php new file mode 100644 index 00000000..6687e7c2 --- /dev/null +++ b/tests/Constraints/DefaultPropertiesTest.php @@ -0,0 +1,153 @@ +validate($inputDecoded, json_decode($schema)); + + $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); + + if ($expectOutput !== null) { + $this->assertEquals($expectOutput, json_encode($inputDecoded)); + } + } + + /** + * @dataProvider getValidTests + */ + public function testValidCasesUsingAssoc($input, $schema, $expectOutput = null) + { + $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)); + } + + /** + * @dataProvider getValidTests + */ + public function testValidCasesUsingAssocWithoutTypeCast($input, $schema, $expectOutput = null) + { + $input = json_decode($input, true); + $factory = new Factory(null, null, Constraint::CHECK_MODE_APPLY_DEFAULTS); + self::testValidCases($input, $schema, $expectOutput, new Validator($factory)); + } + + public function testNoModificationViaReferences() + { + $input = json_decode(''); + $schema = json_decode('{"default":{"propertyOne":"valueOne"}}'); + + $validator = new Validator(); + $validator->validate($input, $schema, Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_APPLY_DEFAULTS); + + $this->assertEquals('{"propertyOne":"valueOne"}', json_encode($input)); + + $input->propertyOne = 'valueTwo'; + $this->assertEquals('valueOne', $schema->default->propertyOne); + } +} diff --git a/tests/Constraints/TypeTest.php b/tests/Constraints/TypeTest.php index 10d3ad21..df8d6dd1 100644 --- a/tests/Constraints/TypeTest.php +++ b/tests/Constraints/TypeTest.php @@ -9,6 +9,7 @@ namespace JsonSchema\Tests\Constraints; +use JsonSchema\Constraints\TypeCheck\LooseTypeCheck; use JsonSchema\Constraints\TypeConstraint; /** @@ -51,6 +52,19 @@ public function testIndefiniteArticleForTypeInTypeCheckErrorMessage($type, $word $this->assertTypeConstraintError(ucwords($label) . " value found, but $wording is required", $constraint); } + /** + * Test uncovered areas of the loose type checker + */ + public function testLooseTypeChecking() + { + $v = new \StdClass(); + $v->property = 'dataOne'; + LooseTypeCheck::propertySet($v, 'property', 'dataTwo'); + $this->assertEquals('dataTwo', $v->property); + $this->assertEquals('dataTwo', LooseTypeCheck::propertyGet($v, 'property')); + $this->assertEquals(1, LooseTypeCheck::propertyCount($v)); + } + /** * Helper to assert an error message *