diff --git a/Normalizer/AbstractObjectNormalizer.php b/Normalizer/AbstractObjectNormalizer.php index 6fec439b9a0..77de6ff3fec 100644 --- a/Normalizer/AbstractObjectNormalizer.php +++ b/Normalizer/AbstractObjectNormalizer.php @@ -611,6 +611,8 @@ private function validateAndDenormalizeLegacy(array $types, string $currentClass if (!$isUnionType && !$isNullable) { throw $e; } + + $expectedTypes[LegacyType::BUILTIN_TYPE_OBJECT === $builtinType && $class ? $class : $builtinType] = true; } catch (ExtraAttributesException $e) { if (!$isUnionType && !$isNullable) { throw $e; @@ -901,6 +903,8 @@ private function validateAndDenormalize(Type $type, string $currentClass, string if (!$type instanceof UnionType) { throw $e; } + + $expectedTypes[TypeIdentifier::OBJECT === $typeIdentifier && $class ? $class : $typeIdentifier->value] = true; } catch (ExtraAttributesException $e) { if (!$type instanceof UnionType) { throw $e; diff --git a/Normalizer/GetSetMethodNormalizer.php b/Normalizer/GetSetMethodNormalizer.php index 17731067ce6..b0ca3b4bd6d 100644 --- a/Normalizer/GetSetMethodNormalizer.php +++ b/Normalizer/GetSetMethodNormalizer.php @@ -169,6 +169,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 9b6ea1c322c..fc1a076fa9c 100644 --- a/Normalizer/ObjectNormalizer.php +++ b/Normalizer/ObjectNormalizer.php @@ -153,6 +153,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 979e95e021e..d200d972c47 100644 --- a/Normalizer/PropertyNormalizer.php +++ b/Normalizer/PropertyNormalizer.php @@ -99,6 +99,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 80788a0d274..67f74d1ef41 100644 --- a/Tests/Normalizer/GetSetMethodNormalizerTest.php +++ b/Tests/Normalizer/GetSetMethodNormalizerTest.php @@ -569,6 +569,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 6b6289aa225..451a784377c 100644 --- a/Tests/Normalizer/ObjectNormalizerTest.php +++ b/Tests/Normalizer/ObjectNormalizerTest.php @@ -25,6 +25,7 @@ use Symfony\Component\Serializer\Exception\NotNormalizableValueException; 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; @@ -42,6 +43,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\OtherSerializedNameDummy; use Symfony\Component\Serializer\Tests\Fixtures\Php74Dummy; use Symfony\Component\Serializer\Tests\Fixtures\Php74DummyPrivate; @@ -64,6 +66,7 @@ use Symfony\Component\Serializer\Tests\Normalizer\Features\TypedPropertiesObject; use Symfony\Component\Serializer\Tests\Normalizer\Features\TypedPropertiesObjectWithGetters; use Symfony\Component\Serializer\Tests\Normalizer\Features\TypeEnforcementTestTrait; +use Symfony\Component\TypeInfo\Type; /** * @author Kévin Dunglas @@ -345,6 +348,27 @@ 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); + + if (class_exists(Type::class) && method_exists(PropertyInfoExtractor::class, 'getType')) { + $this->expectExceptionMessage('The type of the "value" attribute for class "Symfony\Component\Serializer\Tests\Fixtures\DummyWithUnion" must be one of "float", "int" ("string" given).'); + } else { + $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 @@ -1086,6 +1110,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 @@ -1429,6 +1470,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 30b8f85f056..521b8cf009f 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; @@ -585,6 +586,23 @@ public function testDenormalizeWithDiscriminator() ) ); } + + 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