8000 [Serializer] Deserialize union type does not work · Issue #46396 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content
[Serializer] Deserialize union type does not work #46396
Closed
@Gwemox

Description

@Gwemox

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0