From a7d76a1d5c2e92e9e763601c90cae8ba9febcaee Mon Sep 17 00:00:00 2001 From: Christian Grasso Date: Mon, 29 Sep 2025 17:24:36 +0200 Subject: [PATCH 1/2] bug #61887 [Serializer] Fix discriminator class mapping with allow_extra_attributes=false Make sure that the discriminator mapping type property is considered an allowed attribute when the `AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES` context option is set to `false`. --- Normalizer/GetSetMethodNormalizer.php | 4 ++ Normalizer/ObjectNormalizer.php | 4 ++ Normalizer/PropertyNormalizer.php | 4 ++ .../Normalizer/GetSetMethodNormalizerTest.php | 17 +++++++++ Tests/Normalizer/ObjectNormalizerTest.php | 37 +++++++++++++++++++ Tests/Normalizer/PropertyNormalizerTest.php | 18 +++++++++ 6 files changed, 84 insertions(+) diff --git a/Normalizer/GetSetMethodNormalizer.php b/Normalizer/GetSetMethodNormalizer.php index 5ed77bc0e50..b654eb82b07 100644 --- a/Normalizer/GetSetMethodNormalizer.php +++ b/Normalizer/GetSetMethodNormalizer.php @@ -190,6 +190,10 @@ protected function isAllowedAttribute($classOrObject, string $attribute, ?string $class = \is_object($classOrObject) ? $classOrObject::class : $classOrObject; + if ($this->classDiscriminatorResolver?->getMappingForMappedObject($classOrObject)?->getTypeProperty() === $attribute) { + return true; + } + if (!isset(self::$reflectionCache[$class])) { self::$reflectionCache[$class] = new \ReflectionClass($class); } diff --git a/Normalizer/ObjectNormalizer.php b/Normalizer/ObjectNormalizer.php index 2f79cbe1c37..0793d7da1a7 100644 --- a/Normalizer/ObjectNormalizer.php +++ b/Normalizer/ObjectNormalizer.php @@ -168,6 +168,10 @@ protected function isAllowedAttribute($classOrObject, string $attribute, ?string $class = \is_object($classOrObject) ? $classOrObject::class : $classOrObject; + if ($this->classDiscriminatorResolver?->getMappingForMappedObject($classOrObject)?->getTypeProperty() === $attribute) { + return true; + } + if ($context['_read_attributes'] ?? true) { if (!isset(self::$isReadableCache[$class.$attribute])) { self::$isReadableCache[$class.$attribute] = $this->propertyInfoExtractor->isReadable($class, $attribute) || $this->hasAttributeAccessorMethod($class, $attribute) || (\is_object($classOrObject) && $this->propertyAccessor->isReadable($classOrObject, $attribute)); diff --git a/Normalizer/PropertyNormalizer.php b/Normalizer/PropertyNormalizer.php index 7a204522682..a049aec05cf 100644 --- a/Normalizer/PropertyNormalizer.php +++ b/Normalizer/PropertyNormalizer.php @@ -117,6 +117,10 @@ protected function isAllowedAttribute(object|string $classOrObject, string $attr return false; } + if ($this->classDiscriminatorResolver?->getMappingForMappedObject($classOrObject)?->getTypeProperty() === $attribute) { + return true; + } + try { $reflectionProperty = $this->getReflectionProperty($classOrObject, $attribute); } catch (\ReflectionException) { diff --git a/Tests/Normalizer/GetSetMethodNormalizerTest.php b/Tests/Normalizer/GetSetMethodNormalizerTest.php index 4398fbdab8b..4ba602f1077 100644 --- a/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -563,6 +563,23 @@ public function testSupportsAndDenormalizeWithOptionalSetterArgument(array $data $obj = $this->normalizer->denormalize($data, GetSetDummyWithOptionalAndMultipleSetterArgs::class); $this->assertSame($expected, $obj->$method()); } + + public function testDiscriminatorWithAllowExtraAttributesFalse() + { + // Discriminator type property should be allowed with allow_extra_attributes=false + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $discriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + $normalizer = new GetSetMethodNormalizer($classMetadataFactory, null, null, $discriminator); + + $obj = $normalizer->denormalize( + ['type' => 'one'], + GetSetMethodDummyInterface::class, + null, + [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false] + ); + + $this->assertInstanceOf(GetSetMethodDiscriminatedDummyOne::class, $obj); + } } class GetSetDummy diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index ec26acf7328..8ef6ead38f8 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -23,6 +23,7 @@ use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\RuntimeException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; @@ -1061,6 +1062,23 @@ public function testPrecedenceOfAccessorMethods() 'foo' => 'hasFoo', ], $normalizedSwappedHasserIsser); } + + public function testDiscriminatorWithAllowExtraAttributesFalse() + { + // Discriminator type property should be allowed with allow_extra_attributes=false + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $discriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + $normalizer = new ObjectNormalizer($classMetadataFactory, null, null, null, $discriminator); + + $obj = $normalizer->denormalize( + ['type' => 'type_a'], + DiscriminatorDummyInterface::class, + null, + [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false] + ); + + $this->assertInstanceOf(DiscriminatorDummyTypeA::class, $obj); + } } class ProxyObjectDummy extends ObjectDummy @@ -1404,6 +1422,25 @@ public function isolate() } } +#[\Symfony\Component\Serializer\Attribute\DiscriminatorMap( + typeProperty: 'type', + mapping: [ + 'type_a' => DiscriminatorDummyTypeA::class, + 'type_b' => DiscriminatorDummyTypeB::class, + ] +)] +interface DiscriminatorDummyInterface +{ +} + +class DiscriminatorDummyTypeA implements DiscriminatorDummyInterface +{ +} + +class DiscriminatorDummyTypeB implements DiscriminatorDummyInterface +{ +} + class ObjectWithPropertyAndAllAccessorMethods { public function __construct( diff --git a/Tests/Normalizer/PropertyNormalizerTest.php b/Tests/Normalizer/PropertyNormalizerTest.php index 4ff823654ce..7711801ca16 100644 --- a/Tests/Normalizer/PropertyNormalizerTest.php +++ b/Tests/Normalizer/PropertyNormalizerTest.php @@ -22,6 +22,7 @@ use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -551,6 +552,23 @@ public function testDenormalizeWithDiscriminator() $this->assertEquals($denormalized, $normalizer->denormalize(['type' => 'two', 'url' => 'url'], PropertyDummyInterface::class)); } + + public function testDiscriminatorWithAllowExtraAttributesFalse() + { + // Discriminator type property should be allowed with allow_extra_attributes=false + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + $discriminator = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); + $normalizer = new PropertyNormalizer($classMetadataFactory, null, null, $discriminator); + + $obj = $normalizer->denormalize( + ['type' => 'one'], + PropertyDummyInterface::class, + null, + [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false] + ); + + $this->assertInstanceOf(PropertyDiscriminatedDummyOne::class, $obj); + } } class PropertyDummy From 28779bbdb398cac3421d0e51f7ca669e4a27c5ac Mon Sep 17 00:00:00 2001 From: d-mitrofanov-v Date: Wed, 8 Oct 2025 07:24:22 +0300 Subject: [PATCH 2/2] fix unexpected type in denormalization errors when union type used in constructor in xml --- Normalizer/AbstractObjectNormalizer.php | 2 ++ Tests/Normalizer/ObjectNormalizerTest.php | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index fe08440dc81..7c64fc74c13 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -601,6 +601,8 @@ private function validateAndDenormalize(array $types, string $currentClass, stri if (!$isUnionType && !$isNullable) { throw $e; } + + $expectedTypes[Type::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true; } catch (ExtraAttributesException $e) { if (!$isUnionType && !$isNullable) { throw $e; diff --git a/Tests/Normalizer/ObjectNormalizerTest.php b/Tests/Normalizer/ObjectNormalizerTest.php index 8ef6ead38f8..134b33d3651 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -21,6 +21,7 @@ use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\Attribute\Ignore; use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\RuntimeException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; @@ -41,6 +42,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\Attributes\GroupDummy; use Symfony\Component\Serializer\Tests\Fixtures\CircularReferenceDummy; use Symfony\Component\Serializer\Tests\Fixtures\DummyPrivatePropertyWithoutGetter; +use Symfony\Component\Serializer\Tests\Fixtures\DummyWithUnion; use Symfony\Component\Serializer\Tests\Fixtures\FormatAndContextAwareNormalizer; use Symfony\Component\Serializer\Tests\Fixtures\OtherSerializedNameDummy; use Symfony\Component\Serializer\Tests\Fixtures\Php74Dummy; @@ -343,6 +345,22 @@ public function testConstructorWithUnknownObjectTypeHintDenormalize() $normalizer->denormalize($data, DummyWithConstructorInexistingObject::class); } + public function testConstructorWithNotMatchingUnionTypes() + { + $data = [ + 'value' => 'string', + 'value2' => 'string', + ]; + $normalizer = new ObjectNormalizer(new ClassMetadataFactory(new AttributeLoader()), null, null, new PropertyInfoExtractor([], [new ReflectionExtractor()])); + + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('The type of the "value" attribute for class "Symfony\Component\Serializer\Tests\Fixtures\DummyWithUnion" must be one of "int", "float" ("string" given).'); + + $normalizer->denormalize($data, DummyWithUnion::class, 'xml', [ + AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false, + ]); + } + // attributes protected function getNormalizerForAttributes(): ObjectNormalizer