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 b27b1985eb8e..7422c849ddd8 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 e93d7b4cc5bc..c06c19e24199 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 31206ea67d28..ea26589a2b07 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 000000000000..eaf87d48a710 --- /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 0cca05db3341..c2349901fbdf 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