Skip to content
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

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

Closed
Gwemox opened this issue Oct 6, 2022 · 5 comments
Closed

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

Gwemox opened this issue Oct 6, 2022 · 5 comments

Comments

@Gwemox
Copy link
Contributor

Gwemox commented Oct 6, 2022

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

@stof
Copy link
Member

stof commented Oct 6, 2022

Can you send a pull request with that change (and ideally a test covering it) ?

@Gwemox
Copy link
Contributor Author

Gwemox commented Oct 6, 2022

Yes

@Gwemox
Copy link
Contributor Author

Gwemox commented Jan 31, 2023

What can we do to solve this problem?
The PR seem frozen.

@oliviermpp
Copy link

oliviermpp commented Apr 5, 2023

Hello @stof ! Thanks to have took time to check or issue. Could you check out the test that @Gwemox did ? This problem becomes quiet annoying and blocking for us.

@mtarld
Copy link
Contributor

mtarld commented Aug 24, 2023

I tried another approach to solve that issue: #51475

fabpot added a commit that referenced this issue Aug 26, 2023
This PR was merged into the 6.3 branch.

Discussion
----------

[Serializer] Fix union of enum denormalization

| Q             | A
| ------------- | ---
| Branch?       | 6.3
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | Fix #47797
| License       | MIT
| Doc PR        |

Try other types when a `InvalidArgumentException` occurs for a union type.

Commits
-------

11378ef [Serializer] Fix union of enum denormalization
@fabpot fabpot closed this as completed Aug 26, 2023
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

6 participants