Skip to content

[Serializer] fix: deserialization union type of enum #47803

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
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Symfony\Component\Serializer\Encoder\CsvEncoder;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Exception\ExtraAttributesException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException;
Expand Down Expand Up @@ -450,8 +451,7 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
{
$expectedTypes = [];
$isUnionType = \count($types) > 1;
$extraAttributesException = null;
$missingConstructorArgumentException = null;
$exception = null;
foreach ($types as $type) {
if (null === $data && $type->isNullable()) {
return null;
Expand Down Expand Up @@ -585,35 +585,19 @@ private function validateAndDenormalize(array $types, string $currentClass, stri
if (('is_'.$builtinType)($data)) {
return $data;
}
} catch (NotNormalizableValueException $e) {
if (!$isUnionType) {
throw $e;
}
} catch (ExtraAttributesException $e) {
if (!$isUnionType) {
throw $e;
}

if (!$extraAttributesException) {
$extraAttributesException = $e;
}
} catch (MissingConstructorArgumentsException $e) {
} catch (ExceptionInterface $e) {
if (!$isUnionType) {
throw $e;
}

if (!$missingConstructorArgumentException) {
$missingConstructorArgumentException = $e;
if (!$exception && !$e instanceof NotNormalizableValueException) {
$exception = $e;
}
}
}

if ($extraAttributesException) {
throw $extraAttributesException;
}

if ($missingConstructorArgumentException) {
throw $missingConstructorArgumentException;
if ($exception) {
throw $exception;
Copy link
Member

@stof stof Oct 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will now re-throw a NotNormalizableValueException before the DISABLE_TYPE_ENFORCEMENT check

Copy link
Contributor Author

@Gwemox Gwemox Oct 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof I did not understand quite well.
Is it better not to store the NotNormalizableValueException?

Like this :

if (!$exception && !$e instanceof NotNormalizableValueException) {
    $exception = $e;
}

}

if ($context[self::DISABLE_TYPE_ENFORCEMENT] ?? $this->defaultContext[self::DISABLE_TYPE_ENFORCEMENT] ?? false) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ public function denormalize($data, string $type, string $format = null, array $c
return $type::from($data);
} catch (\ValueError $e) {
throw new InvalidArgumentException('The data must belong to a backed enumeration of type '.$type);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case should probably use NotNormalizableValueException even for invalid strings btw.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception was changed here #47128

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this commit is the source of the bug.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yet it was also fixing a bug, please make sure that test added in #47128 keeps passing and/or that the behavior it fixed remains the expected one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All Serializer-related tests, including those in this commit, pass.

} catch (\TypeError $e) {
$enumBackingType = (new \ReflectionEnum($type))->getBackingType()->getName();
throw NotNormalizableValueException::createForUnexpectedDataType('The data must be of the same type as the backed enumeration of type '.$type, $data, [$enumBackingType], $context['deserialization_path'] ?? null, true);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Symfony\Component\Serializer\Tests\Fixtures;

class DummyObjectWithUnionEnumConstructor
{
public function __construct(public StringBackedEnumDummy|IntegerBackedEnumDummy $sub)
{
}
}
49 changes: 49 additions & 0 deletions src/Symfony/Component/Serializer/Tests/SerializerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,13 @@
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberOne;
use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberTwo;
use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithEnumConstructor;
use Symfony\Component\Serializer\Tests\Fixtures\DummyObjectWithUnionEnumConstructor;
use Symfony\Component\Serializer\Tests\Fixtures\FalseBuiltInDummy;
use Symfony\Component\Serializer\Tests\Fixtures\IntegerBackedEnumDummy;
use Symfony\Component\Serializer\Tests\Fixtures\NormalizableTraversableDummy;
use Symfony\Component\Serializer\Tests\Fixtures\Php74Full;
use Symfony\Component\Serializer\Tests\Fixtures\Php80WithPromotedTypedConstructor;
use Symfony\Component\Serializer\Tests\Fixtures\StringBackedEnumDummy;
use Symfony\Component\Serializer\Tests\Fixtures\TraversableDummy;
use Symfony\Component\Serializer\Tests\Normalizer\TestDenormalizer;
use Symfony\Component\Serializer\Tests\Normalizer\TestNormalizer;
Expand Down Expand Up @@ -790,6 +793,52 @@ public function testUnionTypeDeserializableWithoutAllowedExtraAttributes()
]);
}

/**
* @requires PHP 8.1
*/
public function testEnumUnionTypeDeserializable()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);

$serializer = new Serializer(
[
new BackedEnumNormalizer(),
new ObjectNormalizer($classMetadataFactory, null, null, $extractor, new ClassDiscriminatorFromClassMetadata($classMetadataFactory)),
],
['json' => new JsonEncoder()]
);

$actual = $serializer->deserialize('{"sub": 200}', DummyObjectWithUnionEnumConstructor::class, 'json');
$this->assertEquals(new DummyObjectWithUnionEnumConstructor(IntegerBackedEnumDummy::SUCCESS), $actual);

$actual = $serializer->deserialize('{"sub": "GET"}', DummyObjectWithUnionEnumConstructor::class, 'json');
$this->assertEquals(new DummyObjectWithUnionEnumConstructor(StringBackedEnumDummy::GET), $actual);
}

/**
* @requires PHP 8.1
*/
public function testEnumUnionTypeDeserializationWithWrongEnum()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$extractor = new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]);

$serializer = new Serializer(
[
new BackedEnumNormalizer(),
new ObjectNormalizer($classMetadataFactory, null, null, $extractor, new ClassDiscriminatorFromClassMetadata($classMetadataFactory)),
],
['json' => new JsonEncoder()]
);

try {
$serializer->deserialize('{"sub": "INVALID"}', DummyObjectWithUnionEnumConstructor::class, 'json');
} catch (\Throwable $th) {
$this->assertInstanceOf(InvalidArgumentException::class, $th);
}
}

/**
* @requires PHP 8.2
*/
Expand Down