Closed
Description
Symfony version(s) affected
4.4 - 5.4 - 6.1
Description
It's not possible to deserialize an object with a union-type property when Normalizer throw an other exception than NotNormalizableValueException
.
For examples :
- MissingConstructorArgumentsException
- ExtraAttributesException
How to reproduce
composer.json
{
"require": {
"symfony/serializer": "^5.4",
"symfony/property-access": "^6.0"
}
}
index.php
<?php
require_once __DIR__.'/vendor/autoload.php';
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class SubAPropertyConstructor {
public function __construct(public string $toto) {
}
}
class SubA {
public string $foo;
public function __construct() {
}
}
class SubB {
public string $bar;
}
class A {
public SubA|SubB|null $sub;
}
class B {
public SubAPropertyConstructor|SubB|null $sub;
}
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader());
$encoders = [new JsonEncoder()];
$reflectionExtractor = new ReflectionExtractor();
$propertyInfoExtractor = new PropertyInfoExtractor(
[$reflectionExtractor],
[$reflectionExtractor],
[],
[$reflectionExtractor],
[$reflectionExtractor]
);
$normalizers = [new ObjectNormalizer($classMetadataFactory, null, null, $propertyInfoExtractor)];
$serializer = new Serializer($normalizers, $encoders);
$data = '{"sub": {"bar": "Blabla"}}';
$a = $serializer->deserialize($data, A::class, 'json', [
AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false,
]);
var_dump($a);
$b = $serializer->deserialize($data, B::class, 'json', [
AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false,
]);
var_dump($b);
Deserialize A does not work:
$a = $serializer->deserialize($data, A::class, 'json', [
AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false,
]);
var_dump($a);
PHP Fatal error: Uncaught Symfony\Component\Serializer\Exception\ExtraAttributesException: Extra attributes are not allowed ("bar" is unknown). in /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php:420
Stack trace:
#0 /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Serializer.php(238): Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->denormalize()
#1 /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php(556): Symfony\Component\Serializer\Serializer->denormalize()
#2 /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php(387): Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->validateAndDenormalize()
#3 /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Serializer.php(238): Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->denormalize()
#4 /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Serializer.php(151): Symfony\Component\Serializer\Serializer->denormalize()
#5 /home/thibault/projets/bugreporter-serializer/index.php(55): Symfony\Component\Serializer\Serializer->deserialize()
#6 {main}
thrown in /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php on line 420
Deserialize B does not work:
$b = $serializer->deserialize($data, B::class, 'json', [
AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false,
]);
var_dump($b);
PHP Fatal error: Uncaught Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException: Cannot create an instance of "SubAPropertyConstructor" from serialized data because its constructor requires parameter "toto" to be present. in /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Normalizer/AbstractNormalizer.php:403
Stack trace:
#0 /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php(280): Symfony\Component\Serializer\Normalizer\AbstractNormalizer->instantiateObject()
#1 /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php(358): Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->instantiateObject()
#2 /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Serializer.php(238): Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->denormalize()
#3 /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php(556): Symfony\Component\Serializer\Serializer->denormalize()
#4 /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php(387): Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->validateAndDenormalize()
#5 /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Serializer.php(238): Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->denormalize()
#6 /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Serializer.php(151): Symfony\Component\Serializer\Serializer->denormalize()
#7 /home/thibault/projets/bugreporter-serializer/index.php(52): Symfony\Component\Serializer\Serializer->deserialize()
#8 {main}
thrown in /home/thibault/projets/bugreporter-serializer/vendor/symfony/serializer/Normalizer/AbstractNormalizer.php on line 403
Possible Solution
Symfony\Component\Serializer\Normalize\AbstractObjectNormalizer :
/**
* Validates the submitted data and denormalizes it.
*
* @param Type[] $types
* @param mixed $data
*
* @return mixed
*
* @throws NotNormalizableValueException
* @throws LogicException
*/
private function validateAndDenormalize(array $types, string $currentClass, string $attribute, $data, ?string $format, array $context)
{
$expectedTypes = [];
$isUnionType = \count($types) > 1;
foreach ($types as $type) {
...
// This try-catch should cover all NotNormalizableValueException (and all return branches after the first
// exception) so we could try denormalizing all types of an union type. If the target type is not an union
// type, we will just re-throw the catched exception.
// In the case of no denormalization succeeds with an union type, it will fall back to the default exception
// with the acceptable types list.
try {
...
} catch (NotNormalizableValueException $e) {
if (!$isUnionType) {
throw $e;
}
}
}
...
}
Try / catch block on foreach union-types beacause it catches only NotNormalizableValueException
.
The error comes from the fact that in the try a MissingConstructorArgumentsException
is thrown by Symfony\Component\Serializer\Normalize\AbstractNormalizer
.
Possible solution:
try {
...
} catch (Throwable $e) {
if (!$isUnionType) {
throw $e;
}
}
Additional Context
No response