Skip to content

Commit 16a28ff

Browse files
committed
Add CHECK_MODE_ONLY_REQUIRED_DEFAULTS
If CHECK_MODE_ONLY_REQUIRED_DEFAULTS is set, then only apply defaults if they are marked as required.
1 parent f638716 commit 16a28ff

File tree

4 files changed

+126
-60
lines changed

4 files changed

+126
-60
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ third argument to `Validator::validate()`, or can be provided as the third argum
187187
| `Constraint::CHECK_MODE_TYPE_CAST` | Enable fuzzy type checking for associative arrays and objects |
188188
| `Constraint::CHECK_MODE_COERCE_TYPES` | Convert data types to match the schema where possible |
189189
| `Constraint::CHECK_MODE_APPLY_DEFAULTS` | Apply default values from the schema if not set |
190+
| `Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS` | When applying defaults, only set values that are required |
190191
| `Constraint::CHECK_MODE_EXCEPTIONS` | Throw an exception immediately if validation fails |
191192

192193
Please note that using `Constraint::CHECK_MODE_COERCE_TYPES` or `Constraint::CHECK_MODE_APPLY_DEFAULTS`

src/JsonSchema/Constraints/Constraint.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ abstract class Constraint extends BaseConstraint implements ConstraintInterface
2424
{
2525
protected $inlineSchemaProperty = '$schema';
2626

27-
const CHECK_MODE_NONE = 0x00000000;
28-
const CHECK_MODE_NORMAL = 0x00000001;
29-
const CHECK_MODE_TYPE_CAST = 0x00000002;
30-
const CHECK_MODE_COERCE_TYPES = 0x00000004;
31-
const CHECK_MODE_APPLY_DEFAULTS = 0x00000008;
32-
const CHECK_MODE_EXCEPTIONS = 0x00000010;
27+
const CHECK_MODE_NONE = 0x00000000;
28+
const CHECK_MODE_NORMAL = 0x00000001;
29+
const CHECK_MODE_TYPE_CAST = 0x00000002;
30+
const CHECK_MODE_COERCE_TYPES = 0x00000004;
31+
const CHECK_MODE_APPLY_DEFAULTS = 0x00000008;
32+
const CHECK_MODE_EXCEPTIONS = 0x00000010;
33+
const CHECK_MODE_ONLY_REQUIRED_DEFAULTS = 0x00000020;
3334

3435
/**
3536
* Bubble down the path

src/JsonSchema/Constraints/UndefinedConstraint.php

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -111,34 +111,7 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer
111111
}
112112

113113
// Apply default values from schema
114-
if ($this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) {
115-
if (isset($schema->properties) && LooseTypeCheck::isObject($value)) {
116-
// $value is an object or assoc array, and properties are defined - treat as an object
117-
foreach ($schema->properties as $currentProperty => $propertyDefinition) {
118-
if (!LooseTypeCheck::propertyExists($value, $currentProperty) && isset($propertyDefinition->default)) {
119-
if (is_object($propertyDefinition->default)) {
120-
LooseTypeCheck::propertySet($value, $currentProperty, clone $propertyDefinition->default);
121-
} else {
122-
LooseTypeCheck::propertySet($value, $currentProperty, $propertyDefinition->default);
123-
}
124-
}
125-
}
126-
} elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) {
127-
// $value is an array, and items are defined - treat as plain array
128-
foreach ($schema->items as $currentItem => $itemDefinition) {
129-
if (!isset($value[$currentItem]) && isset($itemDefinition->default)) {
130-
if (is_object($itemDefinition->default)) {
131-
$value[$currentItem] = clone $itemDefinition->default;
132-
} else {
133-
$value[$currentItem] = $itemDefinition->default;
134-
}
135-
}
136-
}
137-
} elseif (($value instanceof self || $value === null) && isset($schema->default)) {
138-
// $value is a leaf, not a container - apply the default directly
139-
$value = is_object($schema->default) ? clone $schema->default : $schema->default;
140-
}
141-
}
114+
$this->applyDefaultValues($value, $schema);
142115

143116
// Verify required values
144117
if ($this->getTypeCheck()->isObject($value)) {
@@ -200,6 +173,76 @@ protected function validateCommonProperties(&$value, $schema = null, JsonPointer
200173
}
201174
}
202175

176+
/**
177+
* Apply default values
178+
*
179+
* @param mixed $value
180+
* @param mixed $schema
181+
*/
182+
protected function applyDefaultValues(&$value, $schema)
183+
{
184+
// only apply defaults if feature is enabled
185+
if (!$this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) {
186+
return;
187+
}
188+
189+
// check whether this default should be applied
190+
$shouldApply = function ($definition, $name = null) use ($schema) {
191+
// required-only mode is off
192+
if (!$this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_DEFAULTS)) {
193+
return true;
194+
}
195+
// draft-04 required is set
196+
if (
197+
$name !== null
198+
&& isset($schema->required)
199+
&& is_array($schema->required)
200+
&& in_array($name, $schema->required)
201+
) {
202+
return true;
203+
}
204+
// draft-03 required is set
205+
if (isset($definition->required) && !is_array($definition->required) && $definition->required) {
206+
return true;
207+
}
208+
// default case
209+
return false;
210+
};
211+
212+
// apply defaults if appropriate
213+
if (isset($schema->properties) && LooseTypeCheck::isObject($value)) {
214+
// $value is an object or assoc array, and properties are defined - treat as an object
215+
foreach ($schema->properties as $currentProperty => $propertyDefinition) {
216+
if (
217+
!LooseTypeCheck::propertyExists($value, $currentProperty)
218+
&& isset($propertyDefinition->default)
219+
&& $shouldApply($propertyDefinition, $currentProperty)
220+
) {
221+
// assign default value
222+
if (is_object($propertyDefinition->default)) {
223+
LooseTypeCheck::propertySet($value, $currentProperty, clone $propertyDefinition->default);
224+
} else {
225+
LooseTypeCheck::propertySet($value, $currentProperty, $propertyDefinition->default);
226+
}
227+
}
228+
}
229+
} elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) {
230+
// $value is an array, and items are defined - treat as plain array
231+
foreach ($schema->items as $currentItem => $itemDefinition) {
232+
if (!isset($value[$currentItem]) && isset($itemDefinition->default) && $shouldApply($itemDefinition)) {
233+
if (is_object($itemDefinition->default)) {
234+
$value[$currentItem] = clone $itemDefinition->default;
235+
} else {
236+
$value[$currentItem] = $itemDefinition->default;
237+
}
238+
}
239+
}
240+
} elseif (($value instanceof self || $value === null) && isset($schema->default) && $shouldApply($schema)) {
241+
// $value is a leaf, not a container - apply the default directly
242+
$value = is_object($schema->default) ? clone $schema->default : $schema->default;
243+
}
244+
}
245+
203246
/**
204247
* Validate allOf, anyOf, and oneOf properties
205248
*

tests/Constraints/DefaultPropertiesTest.php

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,95 +19,116 @@ class DefaultPropertiesTest extends VeryBaseTestCase
1919
public function getValidTests()
2020
{
2121
return array(
22-
array(// default value for entire object
22+
array(// #0 default value for entire object
2323
'',
2424
'{"default":"valueOne"}',
2525
'"valueOne"'
2626
),
27-
array(// default value in an empty object
27+
array(// #1 default value in an empty object
2828
'{}',
2929
'{"properties":{"propertyOne":{"default":"valueOne"}}}',
3030
'{"propertyOne":"valueOne"}'
3131
),
32-
array(// default value for top-level property
32+
array(// #2 default value for top-level property
3333
'{"propertyOne":"valueOne"}',
3434
'{"properties":{"propertyTwo":{"default":"valueTwo"}}}',
3535
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
3636
),
37-
array(// default value for sub-property
37+
array(// #3 default value for sub-property
3838
'{"propertyOne":{}}',
3939
'{"properties":{"propertyOne":{"properties":{"propertyTwo":{"default":"valueTwo"}}}}}',
4040
'{"propertyOne":{"propertyTwo":"valueTwo"}}'
4141
),
42-
array(// default value for sub-property with sibling
42+
array(// #4 default value for sub-property with sibling
4343
'{"propertyOne":{"propertyTwo":"valueTwo"}}',
4444
'{"properties":{"propertyOne":{"properties":{"propertyThree":{"default":"valueThree"}}}}}',
4545
'{"propertyOne":{"propertyTwo":"valueTwo","propertyThree":"valueThree"}}'
4646
),
47-
array(// default value for top-level property with type check
47+
array(// #5 default value for top-level property with type check
4848
'{"propertyOne":"valueOne"}',
4949
'{"properties":{"propertyTwo":{"default":"valueTwo","type":"string"}}}',
5050
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
5151
),
52-
array(// default value for top-level property with v3 required check
52+
array(// #6 default value for top-level property with v3 required check
5353
'{"propertyOne":"valueOne"}',
5454
'{"properties":{"propertyTwo":{"default":"valueTwo","required":"true"}}}',
5555
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
5656
),
57-
array(// default value for top-level property with v4 required check
57+
array(// #7 default value for top-level property with v4 required check
5858
'{"propertyOne":"valueOne"}',
5959
'{"properties":{"propertyTwo":{"default":"valueTwo"}},"required":["propertyTwo"]}',
6060
'{"propertyOne":"valueOne","propertyTwo":"valueTwo"}'
6161
),
62-
array(//default value for an already set property
62+
array(// #8 default value for an already set property
6363
'{"propertyOne":"alreadySetValueOne"}',
6464
'{"properties":{"propertyOne":{"default":"valueOne"}}}',
6565
'{"propertyOne":"alreadySetValueOne"}'
6666
),
67-
array(//default item value for an array
67+
array(// #9 default item value for an array
6868
'["valueOne"]',
6969
'{"type":"array","items":[{},{"type":"string","default":"valueTwo"}]}',
7070
'["valueOne","valueTwo"]'
7171
),
72-
array(//default item value for an empty array
72+
array(// #10 default item value for an empty array
7373
'[]',
7474
'{"type":"array","items":[{"type":"string","default":"valueOne"}]}',
7575
'["valueOne"]'
7676
),
77-
array(//property without a default available
77+
array(// #11 property without a default available
7878
'{"propertyOne":"alreadySetValueOne"}',
7979
'{"properties":{"propertyOne":{"type":"string"}}}',
8080
'{"propertyOne":"alreadySetValueOne"}'
8181
),
82-
array(// default property value is an object
82+
array(// #12 default property value is an object
8383
'{"propertyOne":"valueOne"}',
8484
'{"properties":{"propertyTwo":{"default":{}}}}',
8585
'{"propertyOne":"valueOne","propertyTwo":{}}'
8686
),
87-
array(// default item value is an object
87+
array(// #13 default item value is an object
8888
'[]',
8989
'{"type":"array","items":[{"default":{}}]}',
9090
'[{}]'
91+
),
92+
array(// #14 only set required values (draft-04)
93+
'{}',
94+
'{
95+
"properties": {
96+
"propertyOne": {"default": "valueOne"},
97+
"propertyTwo": {"default": "valueTwo"}
98+
},
99+
"required": ["propertyTwo"]
100+
}',
101+
'{"propertyTwo":"valueTwo"}',
102+
Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS
103+
),
104+
array(// #15 only set required values (draft-03)
105+
'{}',
106+
'{
107+
"properties": {
108+
"propertyOne": {"default": "valueOne"},
109+
"propertyTwo": {"default": "valueTwo", "required": true}
110+
}
111+
}',
112+
'{"propertyTwo":"valueTwo"}',
113+
Constraint::CHECK_MODE_ONLY_REQUIRED_DEFAULTS
91114
)
92115
);
93116
}
94117

95118
/**
96119
* @dataProvider getValidTests
97120
*/
98-
public function testValidCases($input, $schema, $expectOutput = null, $validator = null)
121+
public function testValidCases($input, $schema, $expectOutput = null, $checkMode = 0)
99122
{
100123
if (is_string($input)) {
101124
$inputDecoded = json_decode($input);
102125
} else {
103126
$inputDecoded = $input;
104127
}
105128

106-
if ($validator === null) {
107-
$factory = new Factory(null, null, Constraint::CHECK_MODE_APPLY_DEFAULTS);
108-
$validator = new Validator($factory);
109-
}
110-
$validator->validate($inputDecoded, json_decode($schema));
129+
$checkMode |= Constraint::CHECK_MODE_APPLY_DEFAULTS;
130+
$validator = new Validator();
131+
$validator->validate($inputDecoded, json_decode($schema), $checkMode);
111132

112133
$this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true));
113134

@@ -119,22 +140,22 @@ public function testValidCases($input, $schema, $expectOutput = null, $validator
119140
/**
120141
* @dataProvider getValidTests
121142
*/
122-
public function testValidCasesUsingAssoc($input, $schema, $expectOutput = null)
143+
public function testValidCasesUsingAssoc($input, $schema, $expectOutput = null, $checkMode = 0)
123144
{
124145
$input = json_decode($input, true);
125146

126-
$factory = new Factory(null, null, Constraint::CHECK_MODE_TYPE_CAST | Constraint::CHECK_MODE_APPLY_DEFAULTS);
127-
self::testValidCases($input, $schema, $expectOutput, new Validator($factory));
147+
$checkMode |= Constraint::CHECK_MODE_TYPE_CAST;
148+
self::testValidCases($input, $schema, $expectOutput, $checkMode);
128149
}
129150

130151
/**
131152
* @dataProvider getValidTests
132153
*/
133-
public function testValidCasesUsingAssocWithoutTypeCast($input, $schema, $expectOutput = null)
154+
public function testValidCasesUsingAssocWithoutTypeCast($input, $schema, $expectOutput = null, $checkMode = 0)
134155
{
135156
$input = json_decode($input, true);
136-
$factory = new Factory(null, null, Constraint::CHECK_MODE_APPLY_DEFAULTS);
137-
self::testValidCases($input, $schema, $expectOutput, new Validator($factory));
157+
158+
self::testValidCases($input, $schema, $expectOutput, $checkMode);
138159
}
139160

140161
public function testNoModificationViaReferences()

0 commit comments

Comments
 (0)