Repository: OpenAPITools/openapi-generator (2025-07-17T20:30:21Z)

Issue:
[BUG][kotlin-spring] String to Enum conversion fails for enum query parameters with non default enumPropertyNaming
#### Bug Report Checklist

- [x] Have you provided a full/minimal spec to reproduce the issue?
- [x] Have you validated the input using an OpenAPI validator?
- [x] Have you [tested with the latest master](https://github.com/OpenAPITools/openapi-generator/wiki/FAQ#how-to-test-with-the-latest-master-of-openapi-generator) to confirm the issue still exists?
- [x] Have you searched for related issues/PRs?
- [x] What's the actual output vs expected output?
- [ ] [Optional] Sponsorship to speed up the bug fix or feature request ([example](https://github.com/OpenAPITools/openapi-generator/issues/6178))

##### Description
When the `enumPropertyNaming` config option value differs from `original` (e.g. `UPPERCASE`) kotlin-spring generator produces
```kotlin
enum class TestEnumSpec(@get:JsonValue val value: kotlin.String) {

    ABC("abc"),
    DEF("def"),
    GHK("ghk");

    companion object {
        @JvmStatic
        @JsonCreator
        fun forValue(value: kotlin.String): TestEnumSpec {
                return values().first{it -> it.value == value}
        }
    }
}
```
for the mentioned spec.
However `forValue` is not used by Spring by default to convert String input into the `TestEnumSpec` query parameter and the exception is thrown
```
Caused by: java.lang.IllegalArgumentException: No enum constant my.package.TestEnumSpec.abc
	at java.base/java.lang.Enum.valueOf(Enum.java:293) ~[?:?]
	at org.springframework.core.convert.support.StringToEnumConverterFactory$StringToEnum.convert(StringToEnumConverterFactory.java:54) ~[spring-core-6.2.7.jar:6.2.7]
	at org.springframework.core.convert.support.StringToEnumConverterFactory$StringToEnum.convert(StringToEnumConverterFactory.java:39) ~[spring-core-6.2.7.jar:6.2.7]
	at org.springframework.core.convert.support.GenericConversionService$ConverterFactoryAdapter.convert(GenericConversionService.java:415) ~[spring-core-6.2.7.jar:6.2.7]
	at org.springframework.core.convert.support.ConversionUtils.invokeConverter(ConversionUtils.java:41) ~[spring-core-6.2.7.jar:6.2.7]
	at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:182) ~[spring-core-6.2.7.jar:6.2.7]
```

Java Spring generator seem to generate [EnumConverterConfiguration](https://github.com/OpenAPITools/openapi-generator/blob/64ab13a7902a0f153ed0b2b2b627e2d89382769a/modules/openapi-generator/src/main/resources/JavaSpring/converter.mustache) to provide custom converters for the generated enums and force using `forValue` for conversion from `String`

##### openapi-generator version

7.12.0

##### OpenAPI declaration file content or url
schema
```yaml
    TestEnumSpec:
      enum:
        - abc
        - def
        - ghk
      default: abc
```
then user in parameters as
```yaml
        - name: test
          in: query
          schema:
            $ref: "#/components/schemas/TestEnumSpec"
```

##### Steps to reproduce

Create enum schema, use it as query parameter.
Use `enumPropertyNaming=UPPERCASE` config option for generation.

##### Related issues/PRs

The #12874 seems to be the similar issue for Java Spring generator 



Discussion:
There's an additional issue / fix (#17999) in the Java Spring generator which is relevant for a fix in kotlin-spring
Thanks for the fast update



Patch:
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java
index 995c05ae1a1a..14f5faba4946 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java
@@ -20,6 +20,7 @@
 import com.samskivert.mustache.Mustache;
 import com.samskivert.mustache.Mustache.Lambda;
 import com.samskivert.mustache.Template;
+import io.swagger.v3.oas.models.Components;
 import io.swagger.v3.oas.models.OpenAPI;
 import io.swagger.v3.oas.models.Operation;
 import lombok.Getter;
@@ -33,6 +34,7 @@
 import org.openapitools.codegen.model.ModelsMap;
 import org.openapitools.codegen.model.OperationMap;
 import org.openapitools.codegen.model.OperationsMap;
+import org.openapitools.codegen.utils.ModelUtils;
 import org.openapitools.codegen.utils.URLPathUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -768,6 +770,11 @@ public void addOperationToGroup(String tag, String resourcePath, Operation opera
     public void preprocessOpenAPI(OpenAPI openAPI) {
         super.preprocessOpenAPI(openAPI);
 
+        if (SPRING_BOOT.equals(library) && ModelUtils.containsEnums(this.openAPI)) {
+            supportingFiles.add(new SupportingFile("converter.mustache",
+                (sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "EnumConverterConfiguration.kt"));
+        }
+
         if (!additionalProperties.containsKey(TITLE)) {
             // The purpose of the title is for:
             // - README documentation
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java
index 621c91bfa84f..76be2f1f3791 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java
@@ -18,7 +18,6 @@
 package org.openapitools.codegen.languages;
 
 import com.samskivert.mustache.Mustache;
-import io.swagger.v3.oas.models.Components;
 import io.swagger.v3.oas.models.OpenAPI;
 import io.swagger.v3.oas.models.Operation;
 import io.swagger.v3.oas.models.PathItem;
@@ -40,6 +39,7 @@
 import org.openapitools.codegen.templating.mustache.SplitStringLambda;
 import org.openapitools.codegen.templating.mustache.SpringHttpStatusLambda;
 import org.openapitools.codegen.templating.mustache.TrimWhitespaceLambda;
+import org.openapitools.codegen.utils.ModelUtils;
 import org.openapitools.codegen.utils.ProcessUtils;
 import org.openapitools.codegen.utils.URLPathUtils;
 import org.slf4j.Logger;
@@ -648,20 +648,6 @@ public void processOpts() {
         supportsAdditionalPropertiesWithComposedSchema = true;
     }
 
-    private boolean containsEnums() {
-        if (openAPI == null) {
-            return false;
-        }
-
-        Components components = this.openAPI.getComponents();
-        if (components == null || components.getSchemas() == null) {
-            return false;
-        }
-
-        return components.getSchemas().values().stream()
-                .anyMatch(it -> it.getEnum() != null && !it.getEnum().isEmpty());
-    }
-
     private boolean supportLibraryUseTags() {
         return SPRING_BOOT.equals(library) || SPRING_CLOUD_LIBRARY.equals(library);
     }
@@ -696,7 +682,7 @@ public void addOperationToGroup(String tag, String resourcePath, Operation opera
     public void preprocessOpenAPI(OpenAPI openAPI) {
         super.preprocessOpenAPI(openAPI);
 
-        if (SPRING_BOOT.equals(library) && containsEnums()) {
+        if (SPRING_BOOT.equals(library) && ModelUtils.containsEnums(this.openAPI)) {
             supportingFiles.add(new SupportingFile("converter.mustache",
                     (sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "EnumConverterConfiguration.java"));
         }
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java
index 405934531f01..2c79c8a6ca83 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java
@@ -2435,6 +2435,19 @@ public static boolean isMetadataOnlySchema(Schema schema) {
                 schema.getContentSchema() != null);
     }
 
+    /**
+     * Returns true if the OpenAPI specification contains any schemas which are enums.
+     * @param openAPI   OpenAPI specification
+     * @return          true if the OpenAPI specification contains any schemas which are enums.
+     */
+    public static boolean containsEnums(OpenAPI openAPI) {
+        Map<String, Schema> schemaMap = getSchemas(openAPI);
+        if (schemaMap.isEmpty()) {
+            return false;
+        }
+
+        return schemaMap.values().stream().anyMatch(ModelUtils::isEnumSchema);
+    }
 
     @FunctionalInterface
     private interface OpenAPISchemaVisitor {
diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/converter.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/converter.mustache
new file mode 100644
index 000000000000..acdd13073fcb
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/kotlin-spring/converter.mustache
@@ -0,0 +1,38 @@
+package {{configPackage}}
+
+{{#models}}
+    {{#model}}
+        {{#isEnum}}
+import {{modelPackage}}.{{classname}}
+        {{/isEnum}}
+    {{/model}}
+{{/models}}
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.convert.converter.Converter
+
+/**
+ * This class provides Spring Converter beans for the enum models in the OpenAPI specification.
+ *
+ * By default, Spring only converts primitive types to enums using Enum::valueOf, which can prevent
+ * correct conversion if the OpenAPI specification is using an `enumPropertyNaming` other than
+ * `original` or the specification has an integer enum.
+ */
+@Configuration(value = "{{configPackage}}.enumConverterConfiguration")
+class EnumConverterConfiguration {
+
+{{#models}}
+{{#model}}
+{{#isEnum}}
+    @Bean(name = ["{{configPackage}}.EnumConverterConfiguration.{{classVarName}}Converter"])
+    fun {{classVarName}}Converter(): Converter<{{{dataType}}}, {{classname}}> {
+        return object: Converter<{{{dataType}}}, {{classname}}> {
+            override fun convert(source: {{{dataType}}}): {{classname}} = {{classname}}.forValue(source)
+        }
+    }
+{{/isEnum}}
+{{/model}}
+{{/models}}
+
+}
diff --git a/samples/server/petstore/kotlin-springboot-integer-enum/.openapi-generator/FILES b/samples/server/petstore/kotlin-springboot-integer-enum/.openapi-generator/FILES
index 97df5ba6fbbc..d35b88d1309a 100644
--- a/samples/server/petstore/kotlin-springboot-integer-enum/.openapi-generator/FILES
+++ b/samples/server/petstore/kotlin-springboot-integer-enum/.openapi-generator/FILES
@@ -9,5 +9,6 @@ settings.gradle
 src/main/kotlin/org/openapitools/api/ApiUtil.kt
 src/main/kotlin/org/openapitools/api/DefaultApi.kt
 src/main/kotlin/org/openapitools/api/Exceptions.kt
+src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt
 src/main/kotlin/org/openapitools/model/ApiError.kt
 src/main/kotlin/org/openapitools/model/ReasonCode.kt
diff --git a/samples/server/petstore/kotlin-springboot-integer-enum/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt b/samples/server/petstore/kotlin-springboot-integer-enum/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt
new file mode 100644
index 000000000000..8d2f1d585482
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-integer-enum/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt
@@ -0,0 +1,26 @@
+package org.openapitools.configuration
+
+import org.openapitools.model.ReasonCode
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.convert.converter.Converter
+
+/**
+ * This class provides Spring Converter beans for the enum models in the OpenAPI specification.
+ *
+ * By default, Spring only converts primitive types to enums using Enum::valueOf, which can prevent
+ * correct conversion if the OpenAPI specification is using an `enumPropertyNaming` other than
+ * `original` or the specification has an integer enum.
+ */
+@Configuration(value = "org.openapitools.configuration.enumConverterConfiguration")
+class EnumConverterConfiguration {
+
+    @Bean(name = ["org.openapitools.configuration.EnumConverterConfiguration.reasonCodeConverter"])
+    fun reasonCodeConverter(): Converter<kotlin.Int, ReasonCode> {
+        return object: Converter<kotlin.Int, ReasonCode> {
+            override fun convert(source: kotlin.Int): ReasonCode = ReasonCode.forValue(source)
+        }
+    }
+
+}
diff --git a/samples/server/petstore/kotlin-springboot-multipart-request-model/.openapi-generator/FILES b/samples/server/petstore/kotlin-springboot-multipart-request-model/.openapi-generator/FILES
index 24c5ef9ccc87..2ea9e1431b36 100644
--- a/samples/server/petstore/kotlin-springboot-multipart-request-model/.openapi-generator/FILES
+++ b/samples/server/petstore/kotlin-springboot-multipart-request-model/.openapi-generator/FILES
@@ -12,6 +12,7 @@ src/main/kotlin/org/openapitools/SpringDocConfiguration.kt
 src/main/kotlin/org/openapitools/api/ApiUtil.kt
 src/main/kotlin/org/openapitools/api/Exceptions.kt
 src/main/kotlin/org/openapitools/api/MultipartMixedApiController.kt
+src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt
 src/main/kotlin/org/openapitools/model/MultipartMixedRequestMarker.kt
 src/main/kotlin/org/openapitools/model/MultipartMixedStatus.kt
 src/main/resources/application.yaml
diff --git a/samples/server/petstore/kotlin-springboot-multipart-request-model/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt b/samples/server/petstore/kotlin-springboot-multipart-request-model/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt
new file mode 100644
index 000000000000..5aff967ce622
--- /dev/null
+++ b/samples/server/petstore/kotlin-springboot-multipart-request-model/src/main/kotlin/org/openapitools/configuration/EnumConverterConfiguration.kt
@@ -0,0 +1,26 @@
+package org.openapitools.configuration
+
+import org.openapitools.model.MultipartMixedStatus
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.core.convert.converter.Converter
+
+/**
+ * This class provides Spring Converter beans for the enum models in the OpenAPI specification.
+ *
+ * By default, Spring only converts primitive types to enums using Enum::valueOf, which can prevent
+ * correct conversion if the OpenAPI specification is using an `enumPropertyNaming` other than
+ * `original` or the specification has an integer enum.
+ */
+@Configuration(value = "org.openapitools.configuration.enumConverterConfiguration")
+class EnumConverterConfiguration {
+
+    @Bean(name = ["org.openapitools.configuration.EnumConverterConfiguration.multipartMixedStatusConverter"])
+    fun multipartMixedStatusConverter(): Converter<kotlin.String, MultipartMixedStatus> {
+        return object: Converter<kotlin.String, MultipartMixedStatus> {
+            override fun convert(source: kotlin.String): MultipartMixedStatus = MultipartMixedStatus.forValue(source)
+        }
+    }
+
+}


Test patch:
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java
index ba7b73e78ee5..5a0d24cdcf1a 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/spring/KotlinSpringServerCodegenTest.java
@@ -5,6 +5,8 @@
 import io.swagger.v3.oas.models.info.Info;
 import io.swagger.v3.oas.models.servers.Server;
 import io.swagger.v3.parser.core.models.ParseOptions;
+import java.util.HashMap;
+import java.util.function.Consumer;
 import org.apache.commons.io.FileUtils;
 import org.assertj.core.api.Assertions;
 import org.jetbrains.annotations.NotNull;
@@ -12,6 +14,8 @@
 import org.openapitools.codegen.CodegenConstants;
 import org.openapitools.codegen.DefaultGenerator;
 import org.openapitools.codegen.TestUtils;
+import org.openapitools.codegen.config.CodegenConfigurator;
+import org.openapitools.codegen.java.assertions.JavaFileAssert;
 import org.openapitools.codegen.kotlin.KotlinTestUtils;
 import org.openapitools.codegen.languages.KotlinSpringServerCodegen;
 import org.openapitools.codegen.languages.features.CXFServerFeatures;
@@ -31,8 +35,10 @@
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.openapitools.codegen.TestUtils.assertFileContains;
 import static org.openapitools.codegen.TestUtils.assertFileNotContains;
+import static org.openapitools.codegen.languages.SpringCodegen.SPRING_BOOT;
 import static org.openapitools.codegen.languages.features.DocumentationProviderFeatures.ANNOTATION_LIBRARY;
 import static org.openapitools.codegen.languages.features.DocumentationProviderFeatures.DOCUMENTATION_PROVIDER;
 
@@ -748,6 +754,40 @@ public void useBeanValidationGenerateAnnotationsForRequestBody() throws IOExcept
         );
     }
 
+    @Test
+    public void contractWithoutEnumDoesNotContainEnumConverter() throws IOException {
+        Map<String, File> output = generateFromContract("src/test/resources/3_0/generic.yaml");
+
+        assertThat(output).doesNotContainKey("EnumConverterConfiguration.kt");
+    }
+
+    @Test
+    public void contractWithEnumContainsEnumConverter() throws IOException {
+        Map<String, File> output = generateFromContract("src/test/resources/3_0/enum.yaml");
+
+        File enumConverterFile = output.get("EnumConverterConfiguration.kt");
+        assertThat(enumConverterFile).isNotNull();
+        assertFileContains(enumConverterFile.toPath(), "fun typeConverter(): Converter<kotlin.String, Type> {");
+        assertFileContains(enumConverterFile.toPath(), "return object: Converter<kotlin.String, Type> {");
+        assertFileContains(enumConverterFile.toPath(), "override fun convert(source: kotlin.String): Type = Type.forValue(source)");
+    }
+
+    @Test
+    public void contractWithResolvedInnerEnumContainsEnumConverter() throws IOException {
+        Map<String, File> files = generateFromContract(
+            "src/test/resources/3_0/inner_enum.yaml",
+            new HashMap<>(),
+            new HashMap<>(),
+            configurator -> configurator.addInlineSchemaOption("RESOLVE_INLINE_ENUMS", "true")
+        );
+
+        File enumConverterFile = files.get("EnumConverterConfiguration.kt");
+        assertThat(enumConverterFile).isNotNull();
+        assertFileContains(enumConverterFile.toPath(), "fun ponyTypeConverter(): Converter<kotlin.String, PonyType> {");
+        assertFileContains(enumConverterFile.toPath(), "return object: Converter<kotlin.String, PonyType> {");
+        assertFileContains(enumConverterFile.toPath(), "override fun convert(source: kotlin.String): PonyType = PonyType.forValue(source)");
+    }
+
     @Test
     public void givenMultipartFormArray_whenGenerateDelegateAndService_thenParameterIsCreatedAsListOfMultipartFile() throws IOException {
         File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
@@ -1192,4 +1232,54 @@ public void testValidationsInQueryParams_issue21238_Api_Delegate() throws IOExce
                 "@NotNull", "@Valid", "@Pattern(regexp=\"^[a-zA-Z0-9]+[a-zA-Z0-9\\\\.\\\\-_]*[a-zA-Z0-9]+$\")");
     }
 
+    private Map<String, File> generateFromContract(String url) throws IOException {
+        return generateFromContract(url, new HashMap<>(), new HashMap<>());
+    }
+
+    private Map<String, File> generateFromContract(String url, Map<String, Object> additionalProperties) throws IOException {
+        return generateFromContract(url, additionalProperties, new HashMap<>());
+    }
+
+    private Map<String, File> generateFromContract(
+        String url,
+        Map<String, Object> additionalProperties,
+        Map<String, String> generatorPropertyDefaults
+    ) throws IOException {
+        return generateFromContract(url, additionalProperties, generatorPropertyDefaults, codegen -> {
+        });
+    }
+
+    /**
+     * Generate the contract with additional configuration.
+     * <p>
+     * use CodegenConfigurator instead of CodegenConfig for easier configuration like in JavaClientCodeGenTest
+     */
+    private Map<String, File> generateFromContract(
+        String url,
+        Map<String, Object> additionalProperties,
+        Map<String, String> generatorPropertyDefaults,
+        Consumer<CodegenConfigurator> consumer
+    ) throws IOException {
+
+        File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
+        output.deleteOnExit();
+
+        final CodegenConfigurator configurator = new CodegenConfigurator()
+            .setGeneratorName("kotlin-spring")
+            .setAdditionalProperties(additionalProperties)
+            .setValidateSpec(false)
+            .setInputSpec(url)
+            .setLibrary(SPRING_BOOT)
+            .setOutputDir(output.getAbsolutePath());
+
+        consumer.accept(configurator);
+
+        ClientOptInput input = configurator.toClientOptInput();
+        DefaultGenerator generator = new DefaultGenerator();
+        generator.setGenerateMetadata(false);
+        generatorPropertyDefaults.forEach(generator::setGeneratorPropertyDefault);
+
+        return generator.opts(input).generate().stream()
+            .collect(Collectors.toMap(File::getName, Function.identity()));
+    }
 }