From 62d033e32cfa390245758545470c0496f7b180b4 Mon Sep 17 00:00:00 2001 From: soyuka Date: Sat, 27 Jun 2026 16:20:30 +0200 Subject: [PATCH 1/2] fix(openapi): don't apply the global name converter to the generated document PR #8306 wired serializer.name_converter.metadata_aware into the OpenAPI serializer to honor #[SerializedName('$ref')] on Reference. That service carries framework.serializer.name_converter as its fallback, so a globally configured converter (e.g. camelCase_to_snake_case) leaked into the spec, producing keys like operation_id, extension_properties, request_bodies that break openapi-generator-cli. Use a dedicated MetadataAwareNameConverter with no fallback: SerializedName metadata is still honored, the global converter no longer applies. --- .../Bundle/Resources/config/openapi.php | 8 +- tests/Functional/OpenApiNameConverterTest.php | 90 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tests/Functional/OpenApiNameConverterTest.php diff --git a/src/Symfony/Bundle/Resources/config/openapi.php b/src/Symfony/Bundle/Resources/config/openapi.php index 8e51f6418a..136785531a 100644 --- a/src/Symfony/Bundle/Resources/config/openapi.php +++ b/src/Symfony/Bundle/Resources/config/openapi.php @@ -29,8 +29,14 @@ return function (ContainerConfigurator $container) { $services = $container->services(); + // A name converter built from the class metadata factory only (no globally configured fallback), + // so the OpenAPI document is not run through e.g. CamelCaseToSnakeCaseNameConverter while still + // honoring #[SerializedName('$ref')] on Reference::getRef(). See #8306/#8360. + $services->set('api_platform.openapi.name_converter') + ->parent('serializer.name_converter.metadata_aware.abstract'); + $services->set('api_platform.openapi.normalizer', OpenApiNormalizer::class) - ->args([inline_service(Serializer::class)->arg(0, [inline_service(ObjectNormalizer::class)->arg(0, service('serializer.mapping.class_metadata_factory'))->arg(1, service('serializer.name_converter.metadata_aware'))->arg(2, service('api_platform.property_accessor'))->arg(3, service('api_platform.property_info'))])->arg(1, [service('serializer.encoder.json')])]) + ->args([inline_service(Serializer::class)->arg(0, [inline_service(ObjectNormalizer::class)->arg(0, service('serializer.mapping.class_metadata_factory'))->arg(1, service('api_platform.openapi.name_converter'))->arg(2, service('api_platform.property_accessor'))->arg(3, service('api_platform.property_info'))])->arg(1, [service('serializer.encoder.json')])]) ->tag('serializer.normalizer', ['priority' => -795]); $services->alias(OpenApiNormalizer::class, 'api_platform.openapi.normalizer'); diff --git a/tests/Functional/OpenApiNameConverterTest.php b/tests/Functional/OpenApiNameConverterTest.php new file mode 100644 index 0000000000..2ff0828ca3 --- /dev/null +++ b/tests/Functional/OpenApiNameConverterTest.php @@ -0,0 +1,90 @@ + + * + * 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\Tests\Functional; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue8143\ReferenceResponse; +use ApiPlatform\Tests\SetupClassResourcesTrait; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +class OpenApiNameConverterAppKernel extends \AppKernel +{ + public function getCacheDir(): string + { + return parent::getCacheDir().'/openapi_name_converter'; + } + + public function getLogDir(): string + { + return parent::getLogDir().'/openapi_name_converter'; + } + + protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader): void + { + parent::configureContainer($c, $loader); + + $loader->load(static function (ContainerBuilder $container): void { + $container->loadFromExtension('framework', [ + 'serializer' => [ + 'name_converter' => 'serializer.name_converter.camel_case_to_snake_case', + ], + ]); + }); + } +} + +final class OpenApiNameConverterTest extends ApiTestCase +{ + use SetupClassResourcesTrait; + + protected static ?bool $alwaysBootKernel = true; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [ReferenceResponse::class]; + } + + protected static function getKernelClass(): string + { + return OpenApiNameConverterAppKernel::class; + } + + public function testGlobalNameConverterDoesNotLeakIntoOpenApiDocument(): void + { + $response = self::createClient()->request('GET', '/docs', [ + 'headers' => ['Accept' => 'application/vnd.openapi+json'], + ]); + + $this->assertResponseIsSuccessful(); + $json = $response->toArray(); + $content = $response->getContent(); + + // OpenAPI keys must stay camelCase even when a global name converter is configured. + $this->assertStringContainsString('"operationId"', $content); + $this->assertStringNotContainsString('"operation_id"', $content); + $this->assertStringNotContainsString('"extension_properties"', $content); + $this->assertStringNotContainsString('"external_docs"', $content); + $this->assertStringNotContainsString('"request_bodies"', $content); + $this->assertStringNotContainsString('"security_schemes"', $content); + + // The #[SerializedName('$ref')] metadata must still be honored. + $responses = $json['paths']['/issue8143_reference_response']['post']['responses']; + $this->assertArrayHasKey('$ref', $responses['401']); + $this->assertSame('#/components/responses/401', $responses['401']['$ref']); + } +} From d7552e7ed8979eb0a77d034af95fdb9cd6de8343 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Mon, 29 Jun 2026 17:09:33 +0200 Subject: [PATCH 2/2] Apply suggestions from code review Co-authored-by: Antoine Bluchet --- src/Symfony/Bundle/Resources/config/openapi.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Symfony/Bundle/Resources/config/openapi.php b/src/Symfony/Bundle/Resources/config/openapi.php index 136785531a..a45f051f9f 100644 --- a/src/Symfony/Bundle/Resources/config/openapi.php +++ b/src/Symfony/Bundle/Resources/config/openapi.php @@ -29,9 +29,6 @@ return function (ContainerConfigurator $container) { $services = $container->services(); - // A name converter built from the class metadata factory only (no globally configured fallback), - // so the OpenAPI document is not run through e.g. CamelCaseToSnakeCaseNameConverter while still - // honoring #[SerializedName('$ref')] on Reference::getRef(). See #8306/#8360. $services->set('api_platform.openapi.name_converter') ->parent('serializer.name_converter.metadata_aware.abstract');