Skip to content

Commit 7eea9e4

Browse files
fix: Create deep copy before checking each sub schema in oneOf (#791)
This PR creates a deep copy of the value before passing it by reference for validating sub schemas of a `oneOf` with type coercion enabled. Fixes #790
1 parent ec0eab0 commit 7eea9e4

File tree

5 files changed

+178
-5
lines changed

5 files changed

+178
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
### Fixed
1313
- Add required permissions for welcome action ([#789](https://github.com/jsonrainbow/json-schema/pull/789))
1414
- Upgrade php cs fixer to latest ([#783](https://github.com/jsonrainbow/json-schema/pull/783))
15+
- Create deep copy before checking each sub schema in oneOf ([#791](https://github.com/jsonrainbow/json-schema/pull/791))
1516

1617
### Changed
1718
- Used PHPStan's int-mask-of<T> type where applicable ([#779](https://github.com/jsonrainbow/json-schema/pull/779))

src/JsonSchema/Constraints/UndefinedConstraint.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use JsonSchema\Constraints\TypeCheck\LooseTypeCheck;
1616
use JsonSchema\Entity\JsonPointer;
1717
use JsonSchema\Exception\ValidationException;
18+
use JsonSchema\Tool\DeepCopy;
1819
use JsonSchema\Uri\UriResolver;
1920

2021
/**
@@ -352,26 +353,31 @@ protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i
352353

353354
if (isset($schema->oneOf)) {
354355
$allErrors = [];
355-
$matchedSchemas = 0;
356+
$matchedSchemas = [];
356357
$startErrors = $this->getErrors();
358+
$coerce = $this->factory->getConfig(self::CHECK_MODE_COERCE_TYPES);
359+
357360
foreach ($schema->oneOf as $oneOf) {
358361
try {
359362
$this->errors = [];
360-
$this->checkUndefined($value, $oneOf, $path, $i);
361-
if (count($this->getErrors()) == 0) {
362-
$matchedSchemas++;
363+
364+
$oneOfValue = $coerce ? DeepCopy::copyOf($value) : $value;
365+
$this->checkUndefined($oneOfValue, $oneOf, $path, $i);
366+
if (count($this->getErrors()) === 0) {
367+
$matchedSchemas[] = ['schema' => $oneOf, 'value' => $oneOfValue];
363368
}
364369
$allErrors = array_merge($allErrors, array_values($this->getErrors()));
365370
} catch (ValidationException $e) {
366371
// deliberately do nothing here - validation failed, but we want to check
367372
// other schema options in the OneOf field.
368373
}
369374
}
370-
if ($matchedSchemas !== 1) {
375+
if (count($matchedSchemas) !== 1) {
371376
$this->addErrors(array_merge($allErrors, $startErrors));
372377
$this->addError(ConstraintError::ONE_OF(), $path);
373378
} else {
374379
$this->errors = $startErrors;
380+
$value = $matchedSchemas[0]['value'];
375381
}
376382
}
377383
}

src/JsonSchema/Tool/DeepCopy.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonSchema\Tool;
6+
7+
use JsonSchema\Exception\JsonDecodingException;
8+
use JsonSchema\Exception\RuntimeException;
9+
10+
class DeepCopy
11+
{
12+
/**
13+
* @param mixed $input
14+
*
15+
* @return mixed
16+
*/
17+
public static function copyOf($input)
18+
{
19+
$json = json_encode($input);
20+
if (JSON_ERROR_NONE < $error = json_last_error()) {
21+
throw new JsonDecodingException($error);
22+
}
23+
24+
if ($json === false) {
25+
throw new RuntimeException('Failed to encode input to JSON: ' . json_last_error_msg());
26+
}
27+
28+
return json_decode($json, self::isAssociativeArray($input));
29+
}
30+
31+
/**
32+
* @param mixed $input
33+
*/
34+
private static function isAssociativeArray($input): bool
35+
{
36+
return is_array($input) && array_keys($input) !== range(0, count($input) - 1);
37+
}
38+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonSchema\Tests\Constraints;
6+
7+
use JsonSchema\Constraints\Constraint;
8+
9+
class UndefinedConstraintTest extends BaseTestCase
10+
{
11+
/**
12+
* @return array{}
13+
*/
14+
public function getInvalidTests(): array
15+
{
16+
return [];
17+
}
18+
19+
/**
20+
* @return array<string, array{input: string, schema: string, checkMode?: int}>
21+
*/
22+
public function getValidTests(): array
23+
{
24+
return [
25+
'oneOf with type coercion should not affect value passed to each sub schema (#790)' => [
26+
'input' => <<<JSON
27+
{
28+
"id": "LOC1",
29+
"related_locations": [
30+
{
31+
"latitude": "51.047598",
32+
"longitude": "3.729943"
33+
}
34+
]
35+
}
36+
JSON
37+
,
38+
'schema' => <<<JSON
39+
{
40+
"title": "Location",
41+
"type": "object",
42+
"properties": {
43+
"id": {
44+
"type": "string"
45+
},
46+
"related_locations": {
47+
"oneOf": [
48+
{
49+
"type": "null"
50+
},
51+
{
52+
"type": "array",
53+
"items": {
54+
"type": "object",
55+
"properties": {
56+
"latitude": {
57+
"type": "string"
58+
},
59+
"longitude": {
60+
"type": "string"
61+
}
62+
}
63+
}
64+
}
65+
]
66+
}
67+
}
68+
}
69+
JSON
70+
,
71+
'checkMode' => Constraint::CHECK_MODE_COERCE_TYPES
72+
]
73+
];
74+
}
75+
}

tests/Tool/DeepCopyTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonSchema\Tests\Tool;
6+
7+
use JsonSchema\Tool\DeepCopy;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class DeepCopyTest extends TestCase
11+
{
12+
public function testCanDeepCopyObject(): void
13+
{
14+
$input = (object) ['foo' => 'bar'];
15+
16+
$result = DeepCopy::copyOf($input);
17+
18+
self::assertEquals($input, $result);
19+
self::assertNotSame($input, $result);
20+
}
21+
22+
public function testCanDeepCopyObjectWithChildObject(): void
23+
{
24+
$child = (object) ['bar' => 'baz'];
25+
$input = (object) ['foo' => $child];
26+
27+
$result = DeepCopy::copyOf($input);
28+
29+
self::assertEquals($input, $result);
30+
self::assertNotSame($input, $result);
31+
self::assertEquals($input->foo, $result->foo);
32+
self::assertNotSame($input->foo, $result->foo);
33+
}
34+
35+
public function testCanDeepCopyArray(): void
36+
{
37+
$input = ['foo' => 'bar'];
38+
39+
$result = DeepCopy::copyOf($input);
40+
41+
self::assertEquals($input, $result);
42+
}
43+
44+
public function testCanDeepCopyArrayWithNestedArray(): void
45+
{
46+
$nested = ['bar' => 'baz'];
47+
$input = ['foo' => $nested];
48+
49+
$result = DeepCopy::copyOf($input);
50+
51+
self::assertEquals($input, $result);
52+
}
53+
}

0 commit comments

Comments
 (0)