From e016444904891379de49c0cf440e9b3f1752e094 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Thu, 22 May 2025 13:28:58 +0200 Subject: [PATCH] [Serializer] Add support for discriminator map in property normalizer Fixes #60214 Currently it's not possible to serialize an object using the PropertyNormalizer when a DiscriminatorMap attribute is used. It produces the following error: > Symfony\Component\Serializer\Exception\NotNormalizableValueException: Type property "type" not found > for the abstract object "Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface". The ObjectNormalizer overrides the `getAllowedAttributes` from AbstractNormalizer and adds support for discriminators. But the PropertyNormalizer does not do this. Therefore it doesn't work. For now, we copy the logic from ObjectNormalizer to PropertyNormalizer and the problem goes away. --- .../Normalizer/PropertyNormalizer.php | 25 ++++++++++++++ .../Tests/Fixtures/DummyMessageInterface.php | 1 + .../Tests/Fixtures/DummyMessageNumberFour.php | 33 +++++++++++++++++++ .../Serializer/Tests/SerializerTest.php | 24 ++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberFour.php diff --git a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php index 6a5d0acd8904d..7361cf616418d 100644 --- a/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/PropertyNormalizer.php @@ -13,6 +13,7 @@ use Symfony\Component\PropertyAccess\Exception\UninitializedPropertyException; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -221,4 +222,28 @@ private function getReflectionProperty(string|object $classOrObject, string $att } } } + + protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool + { + if (false === $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString)) { + return false; + } + + if (null !== $this->classDiscriminatorResolver) { + $class = \is_object($classOrObject) ? $classOrObject::class : $classOrObject; + if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForMappedObject($classOrObject)) { + $allowedAttributes[] = $attributesAsString ? $discriminatorMapping->getTypeProperty() : new AttributeMetadata($discriminatorMapping->getTypeProperty()); + } + + if (null !== $discriminatorMapping = $this->classDiscriminatorResolver->getMappingForClass($class)) { + $attributes = []; + foreach ($discriminatorMapping->getTypesMapping() as $mappedClass) { + $attributes[] = parent::getAllowedAttributes($mappedClass, $context, $attributesAsString); + } + $allowedAttributes = array_merge($allowedAttributes, ...$attributes); + } + } + + return $allowedAttributes; + } } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php index 31206ea67d289..ea26589a2b072 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageInterface.php @@ -20,6 +20,7 @@ 'one' => DummyMessageNumberOne::class, 'two' => DummyMessageNumberTwo::class, 'three' => DummyMessageNumberThree::class, + 'four' => DummyMessageNumberFour::class, ])] interface DummyMessageInterface { diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberFour.php b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberFour.php new file mode 100644 index 0000000000000..e24e1211e82bf --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberFour.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Serializer\Tests\Fixtures; + +use Symfony\Component\Serializer\Attribute\Ignore; +use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface; + +abstract class SomeAbstract { + #[Ignore] + public function getDescription() + { + return 'Hello, World!'; + } +} + +/** + * @author Samuel Roze + */ +class DummyMessageNumberFour extends SomeAbstract implements DummyMessageInterface +{ + public function __construct(public $one) + { + } +} diff --git a/src/Symfony/Component/Serializer/Tests/SerializerTest.php b/src/Symfony/Component/Serializer/Tests/SerializerTest.php index da5ccc15e4397..86bb8c1fc9dcf 100644 --- a/src/Symfony/Component/Serializer/Tests/SerializerTest.php +++ b/src/Symfony/Component/Serializer/Tests/SerializerTest.php @@ -54,6 +54,7 @@ use Symfony\Component\Serializer\Tests\Fixtures\DenormalizableDummy; use Symfony\Component\Serializer\Tests\Fixtures\DummyFirstChildQuux; use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageInterface; +use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberFour; use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberOne; use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberThree; use Symfony\Component\Serializer\Tests\Fixtures\DummyMessageNumberTwo; @@ -504,6 +505,29 @@ public function testDeserializeAndSerializeNestedAbstractAndInterfacedObjectsWit $this->assertEquals($example, $deserialized); } + public function testDeserializeAndSerializeConstructorAndIgnoreAndInterfacedObjectsWithTheClassMetadataDiscriminator() + { + $example = new DummyMessageNumberFour('Hello'); + + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + + $serializer = new Serializer( + [ + new PropertyNormalizer( + $classMetadataFactory, + null, + new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]), + new ClassDiscriminatorFromClassMetadata($classMetadataFactory), + ), + ], ['json' => new JsonEncoder()], + ); + + $serialized = $serializer->serialize($example, 'json'); + $deserialized = $serializer->deserialize($serialized, DummyMessageInterface::class, 'json'); + + $this->assertEquals($example, $deserialized); + } + public function testExceptionWhenTypeIsNotKnownInDiscriminator() { try {