Skip to content

Commit 8492f10

Browse files
bug #50933 [Serializer] Fix deserializing nested arrays of objects with mixed keys (HypeMC)
This PR was merged into the 5.4 branch. Discussion ---------- [Serializer] Fix deserializing nested arrays of objects with mixed keys | Q | A | ------------- | --- | Branch? | 5.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Tickets | Fix #50675 | License | MIT | Doc PR | - Currently an error is thrown when trying to deserialize the following nested array of objects: ```php use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; $serializer = new Serializer([ new ObjectNormalizer(null, null, null, new PhpDocExtractor()), new ArrayDenormalizer(), ], ['json' => new JsonEncoder()]); class Outer { /** * `@var` array<int|string, Inner> */ public array $inners; } class Inner { public string $name; } $serializer->deserialize('{"inners": {"1": {"name": "One"}, "two": {"name": "Two"}}}', Outer::class, 'json'); ``` ``` Fatal error: Uncaught Symfony\Component\Serializer\Exception\NotNormalizableValueException: The type of the key "two" must be "int" ("string" given). ``` The same happens when using generics, e.g. `ArrayCollection<Inner>`. Commits ------- db0e893 [Serializer] Fix deserializing nested arrays of objects with mixed keys
2 parents 6e0aef6 + db0e893 commit 8492f10

File tree

4 files changed

+90
-16
lines changed

4 files changed

+90
-16
lines changed

src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,7 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
532532
$class = $collectionValueType->getClassName().'[]';
533533

534534
if (\count($collectionKeyType = $type->getCollectionKeyTypes()) > 0) {
535-
[$context['key_type']] = $collectionKeyType;
535+
$context['key_type'] = \count($collectionKeyType) > 1 ? $collectionKeyType : $collectionKeyType[0];
536536
}
537537

538538
$context['value_type'] = $collectionValueType;

src/Symfony/Component/Serializer/Normalizer/ArrayDenormalizer.php

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,15 @@ public function denormalize($data, string $type, string $format = null, array $c
4949

5050
$type = substr($type, 0, -2);
5151

52-
$builtinType = isset($context['key_type']) ? $context['key_type']->getBuiltinType() : null;
52+
$builtinTypes = array_map(static function (Type $keyType) {
53+
return $keyType->getBuiltinType();
54+
}, \is_array($keyType = $context['key_type'] ?? []) ? $keyType : [$keyType]);
55+
5356
foreach ($data as $key => $value) {
5457
$subContext = $context;
5558
$subContext['deserialization_path'] = ($context['deserialization_path'] ?? false) ? sprintf('%s[%s]', $context['deserialization_path'], $key) : "[$key]";
5659

57-
if (null !== $builtinType && !('is_'.$builtinType)($key)) {
58-
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, $builtinType, get_debug_type($key)), $key, [$builtinType], $subContext['deserialization_path'] ?? null, true);
59-
}
60+
$this->validateKeyType($builtinTypes, $key, $subContext['deserialization_path']);
6061

6162
$data[$key] = $this->denormalizer->denormalize($value, $type, $format, $subContext);
6263
}
@@ -102,4 +103,22 @@ public function hasCacheableSupportsMethod(): bool
102103
{
103104
return $this->denormalizer instanceof CacheableSupportsMethodInterface && $this->denormalizer->hasCacheableSupportsMethod();
104105
}
106+
107+
/**
108+
* @param mixed $key
109+
*/
110+
private function validateKeyType(array $builtinTypes, $key, string $path): void
111+
{
112+
if (!$builtinTypes) {
113+
return;
114+
}
115+
116+
foreach ($builtinTypes as $builtinType) {
117+
if (('is_'.$builtinType)($key)) {
118+
return;
119+
}
120+
}
121+
122+
throw NotNormalizableValueException::createForUnexpectedDataType(sprintf('The type of the key "%s" must be "%s" ("%s" given).', $key, implode('", "', $builtinTypes), get_debug_type($key)), $key, $builtinTypes, $path, true);
123+
}
105124
}

src/Symfony/Component/Serializer/Tests/DeserializeNestedArrayOfObjectsTest.php

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ public static function provider()
3535
*/
3636
public function testPropertyPhpDoc($class)
3737
{
38-
// GIVEN
3938
$json = <<<EOF
4039
{
4140
"animals": [
@@ -47,13 +46,62 @@ public function testPropertyPhpDoc($class)
4746
new ObjectNormalizer(null, null, null, new PhpDocExtractor()),
4847
new ArrayDenormalizer(),
4948
], ['json' => new JsonEncoder()]);
50-
// WHEN
51-
/** @var Zoo $zoo */
49+
50+
/** @var Zoo|ZooImmutable $zoo */
5251
$zoo = $serializer->deserialize($json, $class, 'json');
53-
// THEN
52+
5453
self::assertCount(1, $zoo->getAnimals());
5554
self::assertInstanceOf(Animal::class, $zoo->getAnimals()[0]);
5655
}
56+
57+
public function testPropertyPhpDocWithKeyTypes()
58+
{
59+
$json = <<<EOF
60+
{
61+
"animalsInt": [
62+
{"name": "Bug"}
63+
],
64+
"animalsString": {
65+
"animal1": {"name": "Bug"}
66+
},
67+
"animalsUnion": {
68+
"animal2": {"name": "Bug"},
69+
"2": {"name": "Dog"}
70+
},
71+
"animalsGenerics": {
72+
"animal3": {"name": "Bug"},
73+
"3": {"name": "Dog"}
74+
}
75+
}
76+
EOF;
77+
$serializer = new Serializer([
78+
new ObjectNormalizer(null, null, null, new PhpDocExtractor()),
79+
new ArrayDenormalizer(),
80+
], ['json' => new JsonEncoder()]);
81+
82+
/** @var ZooWithKeyTypes $zoo */
83+
$zoo = $serializer->deserialize($json, ZooWithKeyTypes::class, 'json');
84+
85+
self::assertCount(1, $zoo->animalsInt);
86+
self::assertArrayHasKey(0, $zoo->animalsInt);
87+
self::assertInstanceOf(Animal::class, $zoo->animalsInt[0]);
88+
89+
self::assertCount(1, $zoo->animalsString);
90+
self::assertArrayHasKey('animal1', $zoo->animalsString);
91+
self::assertInstanceOf(Animal::class, $zoo->animalsString['animal1']);
92+
93+
self::assertCount(2, $zoo->animalsUnion);
94+
self::assertArrayHasKey('animal2', $zoo->animalsUnion);
95+
self::assertInstanceOf(Animal::class, $zoo->animalsUnion['animal2']);
96+
self::assertArrayHasKey(2, $zoo->animalsUnion);
97+
self::assertInstanceOf(Animal::class, $zoo->animalsUnion[2]);
98+
99+
self::assertCount(2, $zoo->animalsGenerics);
100+
self::assertArrayHasKey('animal3', $zoo->animalsGenerics);
101+
self::assertInstanceOf(Animal::class, $zoo->animalsGenerics['animal3']);
102+
self::assertArrayHasKey(3, $zoo->animalsGenerics);
103+
self::assertInstanceOf(Animal::class, $zoo->animalsGenerics[3]);
104+
}
57105
}
58106

59107
class Zoo
@@ -100,16 +148,23 @@ public function getAnimals(): array
100148
}
101149
}
102150

151+
class ZooWithKeyTypes
152+
{
153+
/** @var array<int, Animal> */
154+
public $animalsInt = [];
155+
/** @var array<string, Animal> */
156+
public $animalsString = [];
157+
/** @var array<int|string, Animal> */
158+
public $animalsUnion = [];
159+
/** @var \stdClass<Animal> */
160+
public $animalsGenerics = [];
161+
}
162+
103163
class Animal
104164
{
105165
/** @var string */
106166
private $name;
107167

108-
public function __construct()
109-
{
110-
echo '';
111-
}
112-
113168
public function getName(): ?string
114169
{
115170
return $this->name;

src/Symfony/Component/Serializer/composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"symfony/http-kernel": "^4.4|^5.0|^6.0",
3535
"symfony/mime": "^4.4|^5.0|^6.0",
3636
"symfony/property-access": "^5.4|^6.0",
37-
"symfony/property-info": "^5.3.13|^6.0",
37+
"symfony/property-info": "^5.4.24|^6.2.11",
3838
"symfony/uid": "^5.3|^6.0",
3939
"symfony/validator": "^4.4|^5.0|^6.0",
4040
"symfony/var-dumper": "^4.4|^5.0|^6.0",
@@ -47,7 +47,7 @@
4747
"phpdocumentor/type-resolver": "<1.4.0",
4848
"symfony/dependency-injection": "<4.4",
4949
"symfony/property-access": "<5.4",
50-
"symfony/property-info": "<5.3.13",
50+
"symfony/property-info": "<5.4.24|>=6,<6.2.11",
5151
"symfony/uid": "<5.3",
5252
"symfony/yaml": "<4.4"
5353
},

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy