From bbaa4951d32d18988a4c66c2ea3774fdc2049f74 Mon Sep 17 00:00:00 2001 From: soyuka Date: Sat, 21 Feb 2026 07:54:05 +0100 Subject: [PATCH] fix(serializer): prevent api_platform_output context from leaking to nested non-resource objects Stores the output/input class in a dedicated context key (api_platform_output_class / api_platform_input_class) instead of relying on resource_class, which gets mutated when processing nested property contexts (e.g. enums, anonymous resources). Fixes #7785 Co-Authored-By: Claude Sonnet 4.6 --- src/Serializer/AbstractItemNormalizer.php | 6 ++++-- .../Tests/AbstractItemNormalizerTest.php | 12 ++++-------- .../ApiResource/DummyDtoNameConverted.php | 4 +++- .../TestBundle/Dto/OutputDtoWithNameConverter.php | 3 +++ tests/Functional/InputOutputNameConverterTest.php | 14 ++++++++++++++ 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 57cddbd579..15f74e6226 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -116,7 +116,7 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return true; } - if (isset($context['api_platform_output']) && ($context['resource_class'] ?? null) === $class) { + if (isset($context['api_platform_output_class']) && $context['api_platform_output_class'] === $class) { return true; } @@ -152,6 +152,7 @@ public function normalize(mixed $data, ?string $format = null, array $context = $context['resource_class'] = $outputClass; $context['api_sub_level'] = true; $context['api_platform_output'] = true; + $context['api_platform_output_class'] = $outputClass; $context[self::ALLOW_EXTRA_ATTRIBUTES] = false; return $this->serializer->normalize($data, $format, $context); @@ -221,7 +222,7 @@ public function supportsDenormalization(mixed $data, string $type, ?string $form return true; } - if (isset($context['api_platform_input']) && ($context['resource_class'] ?? null) === $type) { + if (isset($context['api_platform_input_class']) && $context['api_platform_input_class'] === $type) { return true; } @@ -243,6 +244,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a unset($context['input'], $context['operation'], $context['operation_name'], $context['uri_variables']); $context['resource_class'] = $inputClass; $context['api_platform_input'] = true; + $context['api_platform_input_class'] = $inputClass; try { return $this->serializer->denormalize($data, $inputClass, $format, $context); diff --git a/src/Serializer/Tests/AbstractItemNormalizerTest.php b/src/Serializer/Tests/AbstractItemNormalizerTest.php index cb669c4fba..31802672ae 100644 --- a/src/Serializer/Tests/AbstractItemNormalizerTest.php +++ b/src/Serializer/Tests/AbstractItemNormalizerTest.php @@ -1909,12 +1909,10 @@ public function testSupportsDenormalizationWithApiPlatformInputContext(): void $this->assertFalse($normalizer->supportsDenormalization([], \stdClass::class)); $this->assertTrue($normalizer->supportsDenormalization([], \stdClass::class, null, [ - 'api_platform_input' => true, - 'resource_class' => \stdClass::class, + 'api_platform_input_class' => \stdClass::class, ])); $this->assertFalse($normalizer->supportsDenormalization([], \stdClass::class, null, [ - 'api_platform_input' => true, - 'resource_class' => 'SomeOtherClass', + 'api_platform_input_class' => 'SomeOtherClass', ])); } @@ -1933,12 +1931,10 @@ public function testSupportsNormalizationWithApiPlatformOutputContext(): void $std = new \stdClass(); $this->assertFalse($normalizer->supportsNormalization($std)); $this->assertTrue($normalizer->supportsNormalization($std, null, [ - 'api_platform_output' => true, - 'resource_class' => \stdClass::class, + 'api_platform_output_class' => \stdClass::class, ])); $this->assertFalse($normalizer->supportsNormalization($std, null, [ - 'api_platform_output' => true, - 'resource_class' => 'SomeOtherClass', + 'api_platform_output_class' => 'SomeOtherClass', ])); } } diff --git a/tests/Fixtures/TestBundle/ApiResource/DummyDtoNameConverted.php b/tests/Fixtures/TestBundle/ApiResource/DummyDtoNameConverted.php index e0c54b8cec..dd9b5df8ad 100644 --- a/tests/Fixtures/TestBundle/ApiResource/DummyDtoNameConverted.php +++ b/tests/Fixtures/TestBundle/ApiResource/DummyDtoNameConverted.php @@ -19,6 +19,7 @@ use ApiPlatform\Metadata\Post; use ApiPlatform\Tests\Fixtures\TestBundle\Dto\InputDtoWithNameConverter; use ApiPlatform\Tests\Fixtures\TestBundle\Dto\OutputDtoWithNameConverter; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; #[ApiResource( operations: [ @@ -40,12 +41,13 @@ class DummyDtoNameConverted public function __construct( public ?int $id = null, public ?string $nameConverted = null, + public ?GenderTypeEnum $gender = null, ) { } public static function provide(Operation $operation, array $uriVariables = [], array $context = []): self { - return new self(id: 1, nameConverted: 'converted'); + return new self(id: 1, nameConverted: 'converted', gender: GenderTypeEnum::MALE); } /** diff --git a/tests/Fixtures/TestBundle/Dto/OutputDtoWithNameConverter.php b/tests/Fixtures/TestBundle/Dto/OutputDtoWithNameConverter.php index ec225c17dc..71035b19b6 100644 --- a/tests/Fixtures/TestBundle/Dto/OutputDtoWithNameConverter.php +++ b/tests/Fixtures/TestBundle/Dto/OutputDtoWithNameConverter.php @@ -13,7 +13,10 @@ namespace ApiPlatform\Tests\Fixtures\TestBundle\Dto; +use ApiPlatform\Tests\Fixtures\TestBundle\Enum\GenderTypeEnum; + class OutputDtoWithNameConverter { public ?string $nameConverted = null; + public ?GenderTypeEnum $gender = null; } diff --git a/tests/Functional/InputOutputNameConverterTest.php b/tests/Functional/InputOutputNameConverterTest.php index 1126c66135..fd3cbb460f 100644 --- a/tests/Functional/InputOutputNameConverterTest.php +++ b/tests/Functional/InputOutputNameConverterTest.php @@ -63,4 +63,18 @@ public function testOutputDtoNameConverterIsApplied(): void $this->assertArrayHasKey('name_converted', $data); $this->assertSame('converted', $data['name_converted']); } + + /** + * Reproduces issue #7785: enum properties in output DTOs were serialized as + * {"@type": "...", "@id": "..."} objects instead of their scalar value. + */ + public function testOutputDtoEnumPropertySerializedAsScalar(): void + { + $response = self::createClient()->request('GET', '/dummy_dto_name_converted/1'); + + $this->assertResponseIsSuccessful(); + $data = $response->toArray(); + $this->assertArrayHasKey('gender', $data); + $this->assertSame('male', $data['gender'], 'Enum in output DTO must serialize as its scalar value, not as a resource object.'); + } }