Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Serializer] Deserialize union type does not work #46396

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Gwemox opened this issue May 18, 2022 · 2 comments
Closed

[Serializer] Deserialize union type does not work #46396

Gwemox opened this issue May 18, 2022 · 2 comments

Comments

@Gwemox
Copy link
Contributor

Gwemox commented May 18, 2022

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

@Celine68
Copy link

This would be great indeed !

@Gwemox
Copy link
Contributor Author

Gwemox commented Jun 24, 2022

This is partially fixed by #45861

nicolas-grekas added a commit that referenced this issue Jun 26, 2022
…tor (Gwemox)

This PR was merged into the 4.4 branch.

Discussion
----------

[Serializer] Fix denormalization union types with constructor

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | Fix #46396
| License       | MIT

Fix bug when deserialize union types with constructor. Before that, `MissingConstructorArgumentsException` was thrown even if another type matched.

Is similar to #45861

Commits
-------

33fb153 [Serializer] Fix denormalization union types with constructor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants