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

      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