Skip to content

Commit 37a18d0

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

File tree

8 files changed

+207
-0
lines changed

8 files changed

+207
-0
lines changed

README.md

Lines changed: 32 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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.

src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php

Lines changed: 10 additions & 0 deletions
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+
} else {
35+
$value[$property] = $data;
36+
}
37+
}
38+
39+
3040
public static function propertyExists($value, $property)
3141
{
3242
if (is_object($value)) {

src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php

Lines changed: 5 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,33 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer
118118
}
119119
}
120120

121+
// Apply default values from schema
122+
if ($coerce && $this->factory->getCheckMode() & self::CHECK_MODE_APPLY_DEFAULTS) {
123+
if ($this->getTypeCheck()->isObject($value) && isset($schema->properties)) {
124+
foreach ($schema->properties as $i => $propertyDefinition) {
125+
if (!$this->getTypeCheck()->propertyExists($value, $i) && isset($propertyDefinition->default)) {
126+
$this->getTypeCheck()->propertySet($value, $i, $propertyDefinition->default);
127+
}
128+
}
129+
} elseif ($this->getTypeCheck()->isArray($value)) {
130+
if (isset($schema->properties)) {
131+
foreach ($schema->properties as $i => $propertyDefinition) {
132+
if (!isset($value[$i]) && isset($propertyDefinition->default)) {
133+
$value[$i] = $propertyDefinition->default;
134+
}
135+
}
136+
} elseif (isset($schema->items)) {
137+
foreach ($schema->items as $i => $itemDefinition) {
138+
if (!isset($value[$i]) && isset($itemDefinition->default)) {
139+
$value[$i] = $itemDefinition->default;
140+
}
141+
}
142+
}
143+
} elseif (($value instanceof UndefinedConstraint || $value === null) && isset($schema->default)) {
144+
$value = $schema->default;
145+
}
146+
}
147+
121148
// Verify required values
122149
if ($this->getTypeCheck()->isObject($value)) {
123150
if (!($value instanceof UndefinedConstraint) && isset($schema->required) && is_array($schema->required)) {
Lines changed: 129 additions & 0 deletions
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(// fulfil required property with a default value
37+
'{"propertyOne":"valueOne"}',
38+
'{"properties":{"propertyTwo":{"required":true,"default":"valueTwo"}}}',
39+
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
40+
),
41+
array(// default value for sub-property
42+
'{"propertyOne":{}}',
43+
'{"properties":{"propertyOne":{"properties":{"propertyTwo":{"default":"valueTwo"}}}}}',
44+
'{"propertyOne":{"propertyTwo":"valueTwo"}}'
45+
),
46+
array(// default value for sub-property with sibling
47+
'{"propertyOne":{"propertyTwo":"valueTwo"}}',
48+
'{"properties":{"propertyOne":{"properties":{"propertyThree":{"default":"valueThree"}}}}}',
49+
'{"propertyOne":{"propertyTwo":"valueTwo","propertyThree":"valueThree"}}'
50+
),
51+
array(// default value for top-level property with type check
52+
'{"propertyOne":"valueOne"}',
53+
'{"properties":{"propertyTwo":{"default":"valueTwo","type":"string"}}}',
54+
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
55+
),
56+
array(// default value for top-level property with v3 required check
57+
'{"propertyOne":"valueOne"}',
58+
'{"properties":{"propertyTwo":{"default":"valueTwo","required":"true"}}}',
59+
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
60+
),
61+
array(// default value for top-level property with v4 required check
62+
'{"propertyOne":"valueOne"}',
63+
'{"properties":{"propertyTwo":{"default":"valueTwo"}},"required":["propertyTwo"]}',
64+
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
65+
),
66+
array(//default value for an already set property
67+
'{"propertyOne":"alreadySetValueOne"}',
68+
'{"properties":{"propertyOne":{"default":"valueOne"}}}',
69+
'{"propertyOne":"alreadySetValueOne"}'
70+
),
71+
array(//default value is required
72+
'{"propertyOne":"valueOne"}',
73+
'{"properties":{"propertyTwo":{"default":"valueTwo","required":true}}}',
74+
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
75+
),
76+
array(//default item value for an array
77+
'["valueOne"]',
78+
'{"type":"array","items":[{},{"type":"string","default":"valueTwo"}]}',
79+
'["valueOne","valueTwo"]'
80+
),
81+
array(//default item value for an empty array
82+
'[]',
83+
'{"type":"array","items":[{"type":"string","default":"valueOne"}]}',
84+
'["valueOne"]'
85+
),
86+
array(//property without a default available
87+
'{"propertyOne":"alreadySetValueOne"}',
88+
'{"properties":{"propertyOne":{"type":"string"}}}',
89+
'{"propertyOne":"alreadySetValueOne"}'
90+
)
91+
);
92+
}
93+
94+
/**
95+
* @dataProvider getValidTests
96+
*/
97+
public function testValidCases($input, $schema, $expectOutput = null, $validator = null)
98+
{
99+
if (is_string($input)) {
100+
$inputDecoded = json_decode($input);
101+
} else {
102+
$inputDecoded = $input;
103+
}
104+
105+
if ($validator === null) {
106+
$checkMode = Constraint::CHECK_MODE_APPLY_DEFAULTS;
107+
$validator = new Validator(new Factory(null, null, $checkMode));
108+
}
109+
$validator->coerce($inputDecoded, json_decode($schema));
110+
111+
$this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true));
112+
113+
if ($expectOutput !== null) {
114+
$this->assertEquals($expectOutput, json_encode($inputDecoded));
115+
}
116+
}
117+
118+
/**
119+
* @dataProvider getValidTests
120+
*/
121+
public function testValidCasesUsingAssoc($input, $schema, $expectOutput = null)
122+
{
123+
$input = json_decode($input, true);
124+
125+
$checkMode = Constraint::CHECK_MODE_APPLY_DEFAULTS | Constraint::CHECK_MODE_TYPE_CAST;
126+
$validator = new Validator(new Factory(null, null, $checkMode));
127+
self::testValidCases($input, $schema, $expectOutput, $validator);
128+
}
129+
}

0 commit comments

Comments
 (0)