From f03696e9e278077e687cd7d765f6faa7894c76b4 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Thu, 19 Jun 2025 14:18:57 +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/AbstractObjectNormalizer.php | 25 ++++++++++++++++ .../Normalizer/ObjectNormalizer.php | 24 --------------- .../Tests/Fixtures/DummyMessageInterface.php | 1 + .../Tests/Fixtures/DummyMessageNumberFour.php | 29 +++++++++++++++++++ .../AbstractObjectNormalizerTest.php | 22 ++++++++++++++ 5 files changed, 77 insertions(+), 24 deletions(-) create mode 100644 src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberFour.php diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index b27b1985eb8ef..7422c849ddd80 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -25,6 +25,7 @@ use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Mapping\AttributeMetadata; use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; @@ -765,6 +766,30 @@ protected function createChildContext(array $parentContext, string $attribute, ? return $context; } + 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; + } + /** * Builds the cache key for the attributes cache. * diff --git a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php index e93d7b4cc5bc8..c06c19e241992 100644 --- a/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/ObjectNormalizer.php @@ -164,30 +164,6 @@ protected function setAttributeValue(object $object, string $attribute, mixed $v } } - 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; - } - protected function isAllowedAttribute($classOrObject, string $attribute, ?string $format = null, array $context = []) { if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) { 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..eaf87d48a7101 --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/DummyMessageNumberFour.php @@ -0,0 +1,29 @@ + + * + * 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; + +abstract class SomeAbstract { + #[Ignore] + public function getDescription() + { + return 'Hello, World!'; + } +} + +class DummyMessageNumberFour extends SomeAbstract implements DummyMessageInterface +{ + public function __construct(public $one) + { + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index 0cca05db3341f..c2349901fbdf4 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -42,6 +42,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Normalizer\PropertyNormalizer; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -49,6 +50,8 @@ use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummyFirstChild; use Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummySecondChild; 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\DummySecondChildQuux; use Symfony\Component\Serializer\Tests\Fixtures\DummyString; use Symfony\Component\Serializer\Tests\Fixtures\DummyWithNotNormalizable; @@ -1087,6 +1090,25 @@ public static function provideBooleanTypesData() [['foo' => false], TruePropertyDummy::class], ]; } + + public function testDeserializeAndSerializeConstructorAndIgnoreAndInterfacedObjectsWithTheClassMetadataDiscriminator() + { + $example = new DummyMessageNumberFour('Hello'); + + $classMetadataFactory = new ClassMetadataFactory(new AttributeLoader()); + + $normalizer = new PropertyNormalizer( + $classMetadataFactory, + null, + new PropertyInfoExtractor([], [new PhpDocExtractor(), new ReflectionExtractor()]), + new ClassDiscriminatorFromClassMetadata($classMetadataFactory), + ); + + $serialized = $normalizer->normalize($example, 'json'); + $deserialized = $normalizer->denormalize($serialized, DummyMessageInterface::class, 'json'); + + $this->assertEquals($example, $deserialized); + } } class AbstractObjectNormalizerDummy extends AbstractObjectNormalizer