Skip to content

Commit b547618

Browse files
committed
fix: Create deep copy before checking each sub schema in oneOf
1 parent ec0eab0 commit b547618

File tree

4 files changed

+173
-5
lines changed

4 files changed

+173
-5
lines changed

src/JsonSchema/Constraints/UndefinedConstraint.php

Lines changed: 10 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,30 @@ 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+
357359
foreach ($schema->oneOf as $oneOf) {
358360
try {
359361
$this->errors = [];
360-
$this->checkUndefined($value, $oneOf, $path, $i);
361-
if (count($this->getErrors()) == 0) {
362-
$matchedSchemas++;
362+
363+
$valueDeepCopy = DeepCopy::copyOf($value);
364+
$this->checkUndefined($valueDeepCopy, $oneOf, $path, $i);
365+
if (count($this->getErrors()) === 0) {
366+
$matchedSchemas[] = ['schema' => $oneOf, 'value' => $valueDeepCopy];
363367
}
364368
$allErrors = array_merge($allErrors, array_values($this->getErrors()));
365369
} catch (ValidationException $e) {
366370
// deliberately do nothing here - validation failed, but we want to check
367371
// other schema options in the OneOf field.
368372
}
369373
}
370-
if ($matchedSchemas !== 1) {
374+
if (count($matchedSchemas) !== 1) {
371375
$this->addErrors(array_merge($allErrors, $startErrors));
372376
$this->addError(ConstraintError::ONE_OF(), $path);
373377
} else {
374378
$this->errors = $startErrors;
379+
$value = $matchedSchemas[0]['value'];
375380
}
376381
}
377382
}

src/JsonSchema/Tool/DeepCopy.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
* @return mixed
15+
*/
16+
public static function copyOf($input)
17+
{
18+
$json = json_encode($input);
19+
if (JSON_ERROR_NONE < $error = json_last_error()) {
20+
throw new JsonDecodingException($error);
21+
}
22+
23+
if ($json === false) {
24+
throw new RuntimeException('Failed to encode input to JSON: ' . json_last_error_msg());
25+
}
26+
27+
return json_decode($json, self::isAssociativeArray($input));
28+
}
29+
30+
/**
31+
* @param mixed $input
32+
*/
33+
private static function isAssociativeArray($input): bool
34+
{
35+
return is_array($input) && array_keys($input) !== range(0, count($input) - 1);
36+
}
37+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
'schema' => <<<JSON
38+
{
39+
"title": "Location",
40+
"type": "object",
41+
"properties": {
42+
"id": {
43+
"type": "string"
44+
},
45+
"related_locations": {
46+
"oneOf": [
47+
{
48+
"type": "null"
49+
},
50+
{
51+
"type": "array",
52+
"items": {
53+
"type": "object",
54+
"properties": {
55+
"latitude": {
56+
"type": "string"
57+
},
58+
"longitude": {
59+
"type": "string"
60+
}
61+
}
62+
}
63+
}
64+
]
65+
}
66+
}
67+
}
68+
JSON,
69+
'checkMode' => Constraint::CHECK_MODE_COERCE_TYPES
70+
]
71+
];
72+
}
73+
}

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)