Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to apply default values from the schema #349

Merged
merged 4 commits into from
Feb 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<?php

use JsonSchema\Validator;
use JsonSchema\Constraints\Constraint;

$request = (object)[
'refundAmount'=>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
Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/JsonSchema/Constraints/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace JsonSchema\Constraints;

use JsonSchema\Constraints\Constraint;
use JsonSchema\Exception\InvalidArgumentException;
use JsonSchema\Exception\InvalidConfigException;
use JsonSchema\SchemaStorage;
Expand Down
9 changes: 9 additions & 0 deletions src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
5 changes: 5 additions & 0 deletions src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
48 changes: 47 additions & 1 deletion src/JsonSchema/Constraints/UndefinedConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace JsonSchema\Constraints;

use JsonSchema\Constraints\TypeCheck\LooseTypeCheck;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Uri\UriResolver;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could be refactored with https://github.com/justinrainbow/json-schema/pull/349/files#diff-9aeb900077027f2ca2e0a03af4ea6a2fR117 by using the LooseTypeCheck instead of $this->getTypeCheck()

// $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)) {
Expand Down
153 changes: 153 additions & 0 deletions tests/Constraints/DefaultPropertiesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

/*
* This file is part of the JsonSchema package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace JsonSchema\Tests\Constraints;

use JsonSchema\Constraints\Constraint;
use JsonSchema\Constraints\Factory;
use JsonSchema\SchemaStorage;
use JsonSchema\Validator;

class DefaultPropertiesTest extends VeryBaseTestCase
{
public function getValidTests()
{
return array(
array(// default value for entire object
'',
'{"default":"valueOne"}',
'"valueOne"'
),
array(// default value in an empty object
'{}',
'{"properties":{"propertyOne":{"default":"valueOne"}}}',
'{"propertyOne":"valueOne"}'
),
array(// default value for top-level property
'{"propertyOne":"valueOne"}',
'{"properties":{"propertyTwo":{"default":"valueTwo"}}}',
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
),
array(// default value for sub-property
'{"propertyOne":{}}',
'{"properties":{"propertyOne":{"properties":{"propertyTwo":{"default":"valueTwo"}}}}}',
'{"propertyOne":{"propertyTwo":"valueTwo"}}'
),
array(// 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
'{"propertyOne":"valueOne"}',
'{"properties":{"propertyTwo":{"default":"valueTwo","type":"string"}}}',
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
),
array(// 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
'{"propertyOne":"valueOne"}',
'{"properties":{"propertyTwo":{"default":"valueTwo"}},"required":["propertyTwo"]}',
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
),
array(//default value for an already set property
'{"propertyOne":"alreadySetValueOne"}',
'{"properties":{"propertyOne":{"default":"valueOne"}}}',
'{"propertyOne":"alreadySetValueOne"}'
),
array(//default item value for an array
'["valueOne"]',
'{"type":"array","items":[{},{"type":"string","default":"valueTwo"}]}',
'["valueOne","valueTwo"]'
),
array(//default item value for an empty array
'[]',
'{"type":"array","items":[{"type":"string","default":"valueOne"}]}',
'["valueOne"]'
),
array(//property without a default available
'{"propertyOne":"alreadySetValueOne"}',
'{"properties":{"propertyOne":{"type":"string"}}}',
'{"propertyOne":"alreadySetValueOne"}'
),
array(// default property value is an object
'{"propertyOne":"valueOne"}',
'{"properties":{"propertyTwo":{"default":{}}}}',
'{"propertyOne":"valueOne","propertyTwo":{}}'
),
array(// default item value is an object
'[]',
'{"type":"array","items":[{"default":{}}]}',
'[{}]'
)
);
}

/**
* @dataProvider getValidTests
*/
public function testValidCases($input, $schema, $expectOutput = null, $validator = null)
{
if (is_string($input)) {
$inputDecoded = json_decode($input);
} else {
$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));

$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);
}
}
14 changes: 14 additions & 0 deletions tests/Constraints/TypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace JsonSchema\Tests\Constraints;

use JsonSchema\Constraints\TypeCheck\LooseTypeCheck;
use JsonSchema\Constraints\TypeConstraint;

/**
Expand Down Expand Up @@ -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
*
Expand Down