Skip to content

Commit 14cadd2

Browse files
committed
Add option to apply default values from the schema
1 parent 325a0f8 commit 14cadd2

File tree

9 files changed

+236
-1
lines changed

9 files changed

+236
-1
lines changed

README.md

+32
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,38 @@ is_bool($request->processRefund); // true
7979
is_int($request->refundAmount); // true
8080
```
8181

82+
### Default values
83+
84+
If your schema contains default values, you can have these automatically applied during validation:
85+
86+
```php
87+
<?php
88+
89+
use JsonSchema\Validator;
90+
use JsonSchema\Constraints\Constraint;
91+
92+
$request = (object)[
93+
'refundAmount'=>17
94+
];
95+
96+
$validator = new Validator();
97+
98+
$validator->coerceDefault($request, (object)[
99+
"type"=>"object",
100+
"properties"=>(object)[
101+
"processRefund"=>(object)[
102+
"type"=>"boolean",
103+
"default"=>true
104+
]
105+
]
106+
]); //validates, and sets defaults for missing properties
107+
108+
is_bool($request->processRefund); // true
109+
$request->processRefund; // true
110+
```
111+
112+
*Note that setting default values also enables type coercion.*
113+
82114
### With inline references
83115

84116
```php

src/JsonSchema/Constraints/Constraint.php

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ abstract class Constraint implements ConstraintInterface
2727

2828
const CHECK_MODE_NORMAL = 0x00000001;
2929
const CHECK_MODE_TYPE_CAST = 0x00000002;
30+
const CHECK_MODE_APPLY_DEFAULTS = 0x00000004;
3031

3132
/**
3233
* @var Factory

src/JsonSchema/Constraints/Factory.php

+15
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use JsonSchema\SchemaStorageInterface;
1515
use JsonSchema\Uri\UriRetriever;
1616
use JsonSchema\UriRetrieverInterface;
17+
use JsonSchema\Constraints\Constraint;
1718

1819
/**
1920
* Factory for centralize constraint initialization.
@@ -148,4 +149,18 @@ public function getCheckMode()
148149
{
149150
return $this->checkMode;
150151
}
152+
153+
/**
154+
* Update apply defaults setting in checkmode
155+
*
156+
* @param boolean $applyDefaults
157+
*/
158+
public function setApplyDefaults($applyDefaults = true)
159+
{
160+
if ($applyDefaults) {
161+
$this->checkMode |= Constraint::CHECK_MODE_APPLY_DEFAULTS;
162+
} else {
163+
$this->checkMode &= ~Constraint::CHECK_MODE_APPLY_DEFAULTS;
164+
}
165+
}
151166
}

src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php

+10
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ public static function propertyGet($value, $property)
2727
return $value[$property];
2828
}
2929

30+
public static function propertySet($value, $property, $data)
31+
{
32+
if (is_object($value)) {
33+
$value->{$property} = $data;
34+
}
35+
36+
$value[$property] = $data;
37+
}
38+
39+
3040
public static function propertyExists($value, $property)
3141
{
3242
if (is_object($value)) {

src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ public static function propertyGet($value, $property)
1919
return $value->{$property};
2020
}
2121

22+
public static function propertySet($value, $property, $data)
23+
{
24+
$value->{$property} = $data;
25+
}
26+
2227
public static function propertyExists($value, $property)
2328
{
2429
return property_exists($value, $property);

src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ public static function isArray($value);
1010

1111
public static function propertyGet($value, $property);
1212

13+
public static function propertySet($value, $property, $data);
14+
1315
public static function propertyExists($value, $property);
1416

1517
public static function propertyCount($value);

src/JsonSchema/Constraints/UndefinedConstraint.php

+29-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ public function validateTypes(&$value, $schema = null, JsonPointer $path, $i = n
6666
}
6767

6868
// check object
69-
if ($this->getTypeCheck()->isObject($value)) {
69+
if (TypeCheck\LooseTypeCheck::isObject($value)) { // Fixes failing assoc tests for default values - currently investigating
70+
//if ($this->getTypeCheck()->isObject($value)) { // to find the root cause of this, noting all other assoc tests pass.
7071
$this->checkObject(
7172
$value,
7273
isset($schema->properties) ? $this->factory->getSchemaStorage()->resolveRefSchema($schema->properties) : $schema,
@@ -118,6 +119,33 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer
118119
}
119120
}
120121

122+
// Apply default values from schema
123+
if ($coerce && $this->factory->getCheckMode() & self::CHECK_MODE_APPLY_DEFAULTS) {
124+
if ($this->getTypeCheck()->isObject($value) && isset($schema->properties)) {
125+
foreach ($schema->properties as $i => $propertyDefinition) {
126+
if (!$this->getTypeCheck()->propertyExists($value, $i) && isset($propertyDefinition->default)) {
127+
$this->getTypeCheck()->propertySet($value, $i, $propertyDefinition->default);
128+
}
129+
}
130+
} elseif ($this->getTypeCheck()->isArray($value)) {
131+
if (isset($schema->properties)) {
132+
foreach ($schema->properties as $i => $propertyDefinition) {
133+
if (!isset($value[$i]) && isset($propertyDefinition->default)) {
134+
$value[$i] = $propertyDefinition->default;
135+
}
136+
}
137+
} elseif (isset($schema->items)) {
138+
foreach ($schema->items as $i => $itemDefinition) {
139+
if (!isset($value[$i]) && isset($itemDefinition->default)) {
140+
$value[$i] = $itemDefinition->default;
141+
}
142+
}
143+
}
144+
} elseif (($value instanceof UndefinedConstraint || $value === null) && isset($schema->default)) {
145+
$value = $schema->default;
146+
}
147+
}
148+
121149
// Verify required values
122150
if ($this->getTypeCheck()->isObject($value)) {
123151
if (!($value instanceof UndefinedConstraint) && isset($schema->required) && is_array($schema->required)) {

src/JsonSchema/Validator.php

+13
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,17 @@ public function coerce(&$value, $schema = null, JsonPointer $path = null, $i = n
5252

5353
$this->addErrors(array_unique($validator->getErrors(), SORT_REGULAR));
5454
}
55+
56+
/**
57+
* Does everything that coerce does, but will also set values to their default, if the value is not
58+
* set and a default is available in the schema. Note that the first argumen is passwd by
59+
* reference, so you must pass in a variable.
60+
*
61+
* {@inheritDoc}
62+
*/
63+
public function coerceDefault(&$value, $schema = null, JsonPointer $path = null, $i = null)
64+
{
65+
$this->factory->setApplyDefaults(true);
66+
$this->coerce($value, $schema, $path, $i);
67+
}
5568
}
+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the JsonSchema package.
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace JsonSchema\Tests\Constraints;
11+
use JsonSchema\SchemaStorage;
12+
use JsonSchema\Validator;
13+
use JsonSchema\Constraints\Constraint;
14+
use JsonSchema\Constraints\Factory;
15+
16+
class DefaultPropertiesTest extends VeryBaseTestCase
17+
{
18+
public function getValidTests()
19+
{
20+
return array(
21+
array(// default value for entire object
22+
'',
23+
'{"default":"valueOne"}',
24+
'"valueOne"'
25+
),
26+
array(// default value in an empty object
27+
'{}',
28+
'{"properties":{"propertyOne":{"default":"valueOne"}}}',
29+
'{"propertyOne":"valueOne"}'
30+
),
31+
array(// default value for top-level property
32+
'{"propertyOne":"valueOne"}',
33+
'{"properties":{"propertyTwo":{"default":"valueTwo"}}}',
34+
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
35+
),
36+
array(// default value for sub-property
37+
'{"propertyOne":{}}',
38+
'{"properties":{"propertyOne":{"properties":{"propertyTwo":{"default":"valueTwo"}}}}}',
39+
'{"propertyOne":{"propertyTwo":"valueTwo"}}'
40+
),
41+
array(// default value for sub-property with sibling
42+
'{"propertyOne":{"propertyTwo":"valueTwo"}}',
43+
'{"properties":{"propertyOne":{"properties":{"propertyThree":{"default":"valueThree"}}}}}',
44+
'{"propertyOne":{"propertyTwo":"valueTwo","propertyThree":"valueThree"}}'
45+
),
46+
array(// default value for top-level property with type check
47+
'{"propertyOne":"valueOne"}',
48+
'{"properties":{"propertyTwo":{"default":"valueTwo","type":"string"}}}',
49+
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
50+
),
51+
array(// default value for top-level property with v3 required check
52+
'{"propertyOne":"valueOne"}',
53+
'{"properties":{"propertyTwo":{"default":"valueTwo","required":"true"}}}',
54+
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
55+
),
56+
array(// default value for top-level property with v4 required check
57+
'{"propertyOne":"valueOne"}',
58+
'{"properties":{"propertyTwo":{"default":"valueTwo"}},"required":["propertyTwo"]}',
59+
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
60+
),
61+
array(//default value for an already set property
62+
'{"propertyOne":"alreadySetValueOne"}',
63+
'{"properties":{"propertyOne":{"default":"valueOne"}}}',
64+
'{"propertyOne":"alreadySetValueOne"}'
65+
),
66+
array(//default value is required
67+
'{"propertyOne":"valueOne"}',
68+
'{"properties":{"propertyTwo":{"default":"valueTwo","required":true}}}',
69+
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
70+
),
71+
array(//default item value for an array
72+
'["valueOne"]',
73+
'{"type":"array","items":[{},{"type":"string","default":"valueTwo"}]}',
74+
'["valueOne","valueTwo"]'
75+
),
76+
array(//default item value for an empty array
77+
'[]',
78+
'{"type":"array","items":[{"type":"string","default":"valueOne"}]}',
79+
'["valueOne"]'
80+
),
81+
array(//property without a default available
82+
'{"propertyOne":"alreadySetValueOne"}',
83+
'{"properties":{"propertyOne":{"type":"string"}}}',
84+
'{"propertyOne":"alreadySetValueOne"}'
85+
)
86+
);
87+
}
88+
89+
/**
90+
* @dataProvider getValidTests
91+
*/
92+
public function testValidCases($input, $schema, $expectOutput = null)
93+
{
94+
if (is_string($input)) {
95+
$assoc = false;
96+
$inputDecoded = json_decode($input);
97+
} else {
98+
$assoc = true;
99+
$inputDecoded = $input;
100+
}
101+
102+
$validator = new Validator();
103+
$validator->coerceDefault($inputDecoded, json_decode($schema));
104+
105+
$this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true));
106+
107+
if ($expectOutput !== null) {
108+
$this->assertEquals($expectOutput, json_encode($inputDecoded));
109+
}
110+
}
111+
112+
/**
113+
* @dataProvider getValidTests
114+
*/
115+
public function testValidCasesUsingAssoc($input, $schema, $expectOutput = null)
116+
{
117+
$input = json_decode($input, true);
118+
self::testValidCases($input, $schema, $expectOutput);
119+
}
120+
121+
public function testDontApplyDefaults()
122+
{
123+
$f = new Factory();
124+
$f->setApplyDefaults(false);
125+
126+
$this->assertEquals(0, $f->getCheckMode() & Constraint::CHECK_MODE_APPLY_DEFAULTS);
127+
}
128+
129+
}

0 commit comments

Comments
 (0)