Skip to content

[Serializer] Deserialize union type of BackedEnum does not work  #47797

Closed
@Gwemox

Description

@Gwemox

Symfony version(s) affected

5.4.12 and newer
6.x

Description

Hello,
It becomes impossible to deserialize a union type that contains a BackedEnum, because InvalidArgumentException is not caught.

It seems that a bug is introduced with the commit symfony/serializer@3fc9afe.

How to reproduce

composer.json

{
    "require": {
        "symfony/serializer": "^5.4",
        "symfony/property-access": "^5.4"
    }
}

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\BackedEnumNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;

Enum SubEnumA : string {
    case Toto = 'toto';
    case Toto2 = 'toto2';
}

Enum SubEnumB : string {
    case Tata = 'tata';
    case Tata2 = 'tata2';
}

class A {
    public SubEnumA|SubEnumB $sub;

    public function __construct(SubEnumA|SubEnumB $sub)
    {
        $this->sub = $sub;
    }
}

$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader());
$encoders = [new JsonEncoder()];
$reflectionExtractor = new ReflectionExtractor();
$propertyInfoExtractor = new PropertyInfoExtractor(
    [$reflectionExtractor],
    [$reflectionExtractor],
    [],
    [$reflectionExtractor],
    [$reflectionExtractor]
);
$normalizers = [
    new BackedEnumNormalizer(),
    new ObjectNormalizer($classMetadataFactory, null, null, $propertyInfoExtractor),
];

$serializer = new Serializer($normalizers, $encoders);

$a = new A(SubEnumB::Tata);

$data = $serializer->serialize($a, 'json');
$a = $serializer->deserialize($data, A::class, 'json', [
    AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false,
]);
var_dump($a);

Deserialize A does not work
Expected :

object(A)#35 (1) {
  ["sub"]=>
  enum(SubEnumB::Tata)
}

Result :

PHP Fatal error:  Uncaught Symfony\Component\Serializer\Exception\InvalidArgumentException: The data must belong to a backed enumeration of type SubEnumA in /home/thibault/workspace/mpp-api/vendor/symfony/serializer/Normalizer/BackedEnumNormalizer.php:67
Stack trace:
#0 /home/thibault/workspace/mpp-api/vendor/symfony/serializer/Serializer.php(238): Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer->denormalize()
#1 /home/thibault/workspace/mpp-api/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php(567): Symfony\Component\Serializer\Serializer->denormalize()
#2 /home/thibault/workspace/mpp-api/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php(635): Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->validateAndDenormalize()
#3 /home/thibault/workspace/mpp-api/vendor/symfony/serializer/Normalizer/AbstractNormalizer.php(383): Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->denormalizeParameter()
#4 /home/thibault/workspace/mpp-api/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php(281): Symfony\Component\Serializer\Normalizer\AbstractNormalizer->instantiateObject()
#5 /home/thibault/workspace/mpp-api/vendor/symfony/serializer/Normalizer/AbstractObjectNormalizer.php(363): Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->instantiateObject()
#6 /home/thibault/workspace/mpp-api/vendor/symfony/serializer/Serializer.php(238): Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer->denormalize()
#7 /home/thibault/workspace/mpp-api/vendor/symfony/serializer/Serializer.php(151): Symfony\Component\Serializer\Serializer->denormalize()
#8 /home/thibault/workspace/mpp-api/issue_XXX.php(54): Symfony\Component\Serializer\Serializer->deserialize()
#9 {main}
  thrown in /home/thibault/workspace/mpp-api/vendor/symfony/serializer/Normalizer/BackedEnumNormalizer.php on line 67

Possible Solution

In method denormalize on Symfony\Component\Serializer\NormalizerBackedEnumNormalizer change InvalidArgumentException by NotNormalizableValueException

    /**
     * {@inheritdoc}
     *
     * @throws NotNormalizableValueException
     */
    public function denormalize($data, string $type, string $format = null, array $context = [])
    {
        if (!is_subclass_of($type, \BackedEnum::class)) {
            throw new InvalidArgumentException('The data must belong to a backed enumeration.');
        }

        if (!\is_int($data) && !\is_string($data)) {
            throw NotNormalizableValueException::createForUnexpectedDataType('The data is neither an integer nor a string, you should pass an integer or a string that can be parsed as an enumeration case of type '.$type.'.', $data, [Type::BUILTIN_TYPE_INT, Type::BUILTIN_TYPE_STRING], $context['deserialization_path'] ?? null, true);
        }

        try {
            return $type::from($data);
        } catch (\ValueError $e) {
            throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type);
        }
    }

From:

        try {
            return $type::from($data);
        } catch (\ValueError $e) {
            throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type);
        }

To:

        try {
            return $type::from($data);
        } catch (\ValueError $e) {
            throw new NotNormalizableValueException('The data must belong to a backed enumeration of type '.$type);
        }

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