From 7da2e078806b7f7a413dc7958fc38ba500e9ea25 Mon Sep 17 00:00:00 2001 From: soyuka Date: Tue, 23 Jun 2026 14:24:14 +0200 Subject: [PATCH] fix(serializer): forward DiscriminatorMap defaultType in PropertyMetadataLoader PropertyMetadataLoader rebuilt Symfony's ClassDiscriminatorMapping from the #[DiscriminatorMap] attribute but only forwarded typeProperty and mapping, silently dropping the third constructor argument defaultType (Symfony 7.1+). The bug only surfaces with a warmed serializer mapping cache (cache:warmup in prod). In dev/test the metadata is built lazily through Symfony's AttributeLoader, which forwards defaultType, so it stays invisible. With the default lost, AbstractItemNormalizer::getClassDiscriminatorResolvedClass() sees a null defaultType and throws "Type property ... not found for the abstract object". Forward the third argument, guarded so it stays compatible with symfony/serializer ^6.4 where the attribute exposes no defaultType. Fixes #8345 --- .../Mapping/Loader/PropertyMetadataLoader.php | 3 ++- .../Model/AbstractWithDiscriminator.php | 21 +++++++++++++++++++ .../Model/ConcreteWithDiscriminator.php | 18 ++++++++++++++++ .../Loader/PropertyMetadataLoaderTest.php | 19 +++++++++++++++++ 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/Serializer/Tests/Fixtures/Model/AbstractWithDiscriminator.php create mode 100644 src/Serializer/Tests/Fixtures/Model/ConcreteWithDiscriminator.php diff --git a/src/Serializer/Mapping/Loader/PropertyMetadataLoader.php b/src/Serializer/Mapping/Loader/PropertyMetadataLoader.php index 408ffdb0c4e..03c806e908b 100644 --- a/src/Serializer/Mapping/Loader/PropertyMetadataLoader.php +++ b/src/Serializer/Mapping/Loader/PropertyMetadataLoader.php @@ -71,7 +71,8 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool if ($attribute instanceof DiscriminatorMap) { $classMetadata->setClassDiscriminatorMapping(new ClassDiscriminatorMapping( method_exists($attribute, 'getTypeProperty') ? $attribute->getTypeProperty() : $attribute->typeProperty, - method_exists($attribute, 'getMapping') ? $attribute->getMapping() : $attribute->mapping + method_exists($attribute, 'getMapping') ? $attribute->getMapping() : $attribute->mapping, + method_exists($attribute, 'getDefaultType') ? $attribute->getDefaultType() : ($attribute->defaultType ?? null), )); continue; } diff --git a/src/Serializer/Tests/Fixtures/Model/AbstractWithDiscriminator.php b/src/Serializer/Tests/Fixtures/Model/AbstractWithDiscriminator.php new file mode 100644 index 00000000000..2605a504e0e --- /dev/null +++ b/src/Serializer/Tests/Fixtures/Model/AbstractWithDiscriminator.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Serializer\Tests\Fixtures\Model; + +use Symfony\Component\Serializer\Attribute\DiscriminatorMap; + +#[DiscriminatorMap(typeProperty: 'discr', mapping: ['concrete' => ConcreteWithDiscriminator::class], defaultType: 'concrete')] +abstract class AbstractWithDiscriminator +{ +} diff --git a/src/Serializer/Tests/Fixtures/Model/ConcreteWithDiscriminator.php b/src/Serializer/Tests/Fixtures/Model/ConcreteWithDiscriminator.php new file mode 100644 index 00000000000..0f53adec4c4 --- /dev/null +++ b/src/Serializer/Tests/Fixtures/Model/ConcreteWithDiscriminator.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Serializer\Tests\Fixtures\Model; + +class ConcreteWithDiscriminator extends AbstractWithDiscriminator +{ +} diff --git a/src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php b/src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php index c4897bbaff5..8352fdb7e59 100644 --- a/src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php +++ b/src/Serializer/Tests/Mapping/Loader/PropertyMetadataLoaderTest.php @@ -16,9 +16,11 @@ use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; use ApiPlatform\Serializer\Mapping\Loader\PropertyMetadataLoader; +use ApiPlatform\Serializer\Tests\Fixtures\Model\AbstractWithDiscriminator; use ApiPlatform\Serializer\Tests\Fixtures\Model\HasRelation; use ApiPlatform\Serializer\Tests\Fixtures\Model\Relation; use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; use Symfony\Component\Serializer\Mapping\ClassMetadata; final class PropertyMetadataLoaderTest extends TestCase @@ -55,4 +57,21 @@ public function testCreateMappingForAClass(): void $this->assertArrayHasKey('name', $attributesMetadata); $this->assertEquals(['read'], $attributesMetadata['name']->getGroups()); } + + public function testForwardsDiscriminatorDefaultType(): void + { + if (!method_exists(ClassDiscriminatorMapping::class, 'getDefaultType')) { // @phpstan-ignore-line + $this->markTestSkipped('ClassDiscriminatorMapping::getDefaultType() requires symfony/serializer 7.1+.'); + } + + $coll = $this->createStub(PropertyNameCollectionFactoryInterface::class); + $coll->method('create')->willReturn(new PropertyNameCollection([])); + $loader = new PropertyMetadataLoader($coll); + $classMetadata = new ClassMetadata(AbstractWithDiscriminator::class); + $loader->loadClassMetadata($classMetadata); + + $mapping = $classMetadata->getClassDiscriminatorMapping(); + $this->assertNotNull($mapping); + $this->assertSame('concrete', $mapping->getDefaultType()); + } }