diff --git a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php index 00d2e3b00ef83..d3036da0c6c0d 100644 --- a/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php +++ b/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php @@ -1159,6 +1159,10 @@ private function getMappedClass(array $data, string $class, array $context): str throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('Type property "%s" not found for the abstract object "%s".', $mapping->getTypeProperty(), $class), null, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false); } + if (!is_string($type) && (!is_object($type) || !method_exists($type, '__toString'))) { + throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type property "%s" for the abstract object "%s" must be a string.', $mapping->getTypeProperty(), $class), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), false); + } + if (null === $mappedClass = $mapping->getClassForType($type)) { throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type "%s" is not a valid value.', $type), $type, ['string'], isset($context['deserialization_path']) ? $context['deserialization_path'].'.'.$mapping->getTypeProperty() : $mapping->getTypeProperty(), true); } diff --git a/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/AbstractDummyFirstChild.php b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/AbstractDummyFirstChild.php index b5cd2faba24ee..cb9cb0abbe347 100644 --- a/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/AbstractDummyFirstChild.php +++ b/src/Symfony/Component/Serializer/Tests/Fixtures/Attributes/AbstractDummyFirstChild.php @@ -16,6 +16,7 @@ class AbstractDummyFirstChild extends AbstractDummy { public $bar; + public $baz; /** @var DummyFirstChildQuux|null */ public $quux; diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php index 270b65f33ef66..bc9a956e4781d 100644 --- a/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/AbstractObjectNormalizerTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Serializer\Tests\Normalizer; use PHPUnit\Framework\TestCase; +use stdClass; use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; @@ -525,6 +526,74 @@ private function getDenormalizerForStringCollection() return $denormalizer; } + /** + * @dataProvider provideInvalidDiscriminatorTypes + */ + public function testDenormalizeWithDiscriminatorMapHandlesInvalidTypeValue(mixed $typeValue, bool $shouldFail) + { + if ($shouldFail) { + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage( + 'The type property "type" for the abstract object "Symfony\Component\Serializer\Tests\Fixtures\Attributes\AbstractDummy" must be a string.' + ); + } + + $factory = new ClassMetadataFactory(new AttributeLoader()); + + $loaderMock = new class implements ClassMetadataFactoryInterface { + public function getMetadataFor($value): ClassMetadataInterface + { + if (AbstractDummy::class === $value) { + return new ClassMetadata( + AbstractDummy::class, + new ClassDiscriminatorMapping('type', [ + 'first' => AbstractDummyFirstChild::class, + 'second' => AbstractDummySecondChild::class, + ]) + ); + } + + throw new InvalidArgumentException(); + } + + public function hasMetadataFor($value): bool + { + return AbstractDummy::class === $value; + } + }; + + $discriminatorResolver = new ClassDiscriminatorFromClassMetadata($loaderMock); + $normalizer = new AbstractObjectNormalizerDummy($factory, null, new PhpDocExtractor(), $discriminatorResolver); + $serializer = new Serializer([$normalizer]); + $normalizer->setSerializer($serializer); + $normalizedData = $normalizer->denormalize(['foo' => 'foo', 'baz' => 'baz', 'quux' => ['value' => 'quux'], 'type' => $typeValue], AbstractDummy::class); + + if ($shouldFail) { + $this->fail('Expected exception not thrown.'); + } + + $this->assertInstanceOf(DummyFirstChildQuux::class, $normalizedData->quux); + } + + public function provideInvalidDiscriminatorTypes() + { + $toStringObject = new class() { + public function __toString() + { + return 'first'; + } + }; + + return [ + [[], true], + [new StdClass(), true], + [123, true], + [false, true], + ['first', false], + [$toStringObject, false], + ]; + } + public function testDenormalizeWithDiscriminatorMapUsesCorrectClassname() { $factory = new ClassMetadataFactory(new AttributeLoader());