diff --git a/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php b/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php index 50c2b66de7..316ec2cbce 100644 --- a/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php +++ b/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php @@ -43,6 +43,7 @@ final class SchemaPropertyMetadataFactory implements PropertyMetadataFactoryInte public function __construct( ResourceClassResolverInterface $resourceClassResolver, private readonly ?PropertyMetadataFactoryInterface $decorated = null, + private readonly bool $includeNullInNullableEnum = true, ) { $this->resourceClassResolver = $resourceClassResolver; } @@ -184,7 +185,9 @@ private function applyNullability(array $schema, bool $isNullable): array $schema['type'] = \is_array($currentType) ? array_merge($currentType, ['null']) : [$currentType, 'null']; if (isset($schema['enum'])) { - $schema['enum'][] = null; + if ($this->includeNullInNullableEnum) { + $schema['enum'][] = null; + } return $schema; } diff --git a/src/JsonSchema/Tests/Metadata/Property/Factory/SchemaPropertyMetadataFactoryTest.php b/src/JsonSchema/Tests/Metadata/Property/Factory/SchemaPropertyMetadataFactoryTest.php index 901b7ed7ef..81062289b3 100644 --- a/src/JsonSchema/Tests/Metadata/Property/Factory/SchemaPropertyMetadataFactoryTest.php +++ b/src/JsonSchema/Tests/Metadata/Property/Factory/SchemaPropertyMetadataFactoryTest.php @@ -41,6 +41,37 @@ public function testEnum(): void $this->assertEquals(['type' => ['integer', 'null'], 'enum' => [1, 2, null]], $apiProperty->getSchema()); } + public function testEnumNullableIncludesNullByDefault(): void + { + $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); + $apiProperty = new ApiProperty(nativeType: Type::nullable(Type::enum(IntEnumAsIdentifier::class))); + $decorated = $this->createMock(PropertyMetadataFactoryInterface::class); + $decorated->expects($this->once())->method('create')->with(DummyWithEnum::class, 'intEnumAsIdentifier')->willReturn($apiProperty); + + // explicitly passing true (same as default) must still add null to enum + $schemaPropertyMetadataFactory = new SchemaPropertyMetadataFactory($resourceClassResolver, $decorated, includeNullInNullableEnum: true); + $apiProperty = $schemaPropertyMetadataFactory->create(DummyWithEnum::class, 'intEnumAsIdentifier'); + + $this->assertEquals(['type' => ['integer', 'null'], 'enum' => [1, 2, null]], $apiProperty->getSchema()); + } + + public function testEnumNullableExcludesNullWhenDisabled(): void + { + $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); + $apiProperty = new ApiProperty(nativeType: Type::nullable(Type::enum(IntEnumAsIdentifier::class))); + $decorated = $this->createMock(PropertyMetadataFactoryInterface::class); + $decorated->expects($this->once())->method('create')->with(DummyWithEnum::class, 'intEnumAsIdentifier')->willReturn($apiProperty); + + // with includeNullInNullableEnum=false, null must NOT appear in the enum array + $schemaPropertyMetadataFactory = new SchemaPropertyMetadataFactory($resourceClassResolver, $decorated, includeNullInNullableEnum: false); + $apiProperty = $schemaPropertyMetadataFactory->create(DummyWithEnum::class, 'intEnumAsIdentifier'); + + $schema = $apiProperty->getSchema(); + $this->assertEquals(['integer', 'null'], $schema['type']); + $this->assertEquals([1, 2], $schema['enum']); + $this->assertNotContains(null, $schema['enum']); + } + public function testWithCustomOpenApiContext(): void { $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php index aaca8987c0..cf9356f421 100644 --- a/src/Laravel/ApiPlatformProvider.php +++ b/src/Laravel/ApiPlatformProvider.php @@ -370,7 +370,8 @@ public function register(): void $app->make(ResourceClassResolverInterface::class) ), $app->make(ResourceClassResolverInterface::class) - ) + ), + (bool) $config->get('api-platform.openapi.include_null_in_nullable_enum', true), ), true === $config->get('app.debug') ? 'array' : $config->get('api-platform.cache', 'file') ); diff --git a/src/Laravel/config/api-platform.php b/src/Laravel/config/api-platform.php index c89c853be2..b841a63109 100644 --- a/src/Laravel/config/api-platform.php +++ b/src/Laravel/config/api-platform.php @@ -166,6 +166,7 @@ // 'openapi' => [ // 'tags' => [], + // 'include_null_in_nullable_enum' => true, // Whether null is included in the enum array for nullable enum properties in the JSON Schema. // ], 'url_generation_strategy' => UrlGeneratorInterface::ABS_PATH, diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index ff3757f933..d1c0d02576 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -1103,6 +1103,7 @@ private function registerOpenApiConfiguration(ContainerBuilder $container, array $container->setParameter('api_platform.openapi.license.url', $config['openapi']['license']['url']); $container->setParameter('api_platform.openapi.license.identifier', $config['openapi']['license']['identifier']); $container->setParameter('api_platform.openapi.overrideResponses', $config['openapi']['overrideResponses']); + $container->setParameter('api_platform.openapi.include_null_in_nullable_enum', $config['openapi']['include_null_in_nullable_enum']); $tags = []; foreach ($config['openapi']['tags'] as $tag) { diff --git a/src/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/DependencyInjection/Configuration.php index 012f0f1aa2..510dc487ec 100644 --- a/src/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/DependencyInjection/Configuration.php @@ -573,6 +573,7 @@ private function addOpenApiSection(ArrayNodeDefinition $rootNode): void ->info('To pass extra configuration to Scalar API Reference, like theme or darkMode.') ->end() ->booleanNode('overrideResponses')->defaultTrue()->info('Whether API Platform adds automatic responses to the OpenAPI documentation.')->end() + ->booleanNode('include_null_in_nullable_enum')->defaultTrue()->info('Whether null is included in the enum array for nullable enum properties in the JSON Schema.')->end() ->scalarNode('error_resource_class')->defaultNull()->info('The class used to represent errors in the OpenAPI documentation.')->end() ->scalarNode('validation_error_resource_class')->defaultNull()->info('The class used to represent validation errors in the OpenAPI documentation.')->end() ->end() diff --git a/src/Symfony/Bundle/Resources/config/json_schema.php b/src/Symfony/Bundle/Resources/config/json_schema.php index b1a21cf8c1..2e6ee9ab7a 100644 --- a/src/Symfony/Bundle/Resources/config/json_schema.php +++ b/src/Symfony/Bundle/Resources/config/json_schema.php @@ -47,6 +47,7 @@ ->args([ service('api_platform.resource_class_resolver'), service('api_platform.json_schema.metadata.property.metadata_factory.schema.inner'), + param('api_platform.openapi.include_null_in_nullable_enum'), ]); $services->set('api_platform.json_schema.backward_compatible_schema_factory', BackwardCompatibleSchemaFactory::class) diff --git a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index fced5378b7..a37c8ad3e4 100644 --- a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -222,6 +222,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm ], 'swagger_ui_extra_configuration' => [], 'overrideResponses' => true, + 'include_null_in_nullable_enum' => true, 'tags' => [], 'error_resource_class' => null, 'validation_error_resource_class' => null,