From d053a5beb76578dc78ad5cb2028c59b5aafac2bb Mon Sep 17 00:00:00 2001 From: dijkstrar <52031414+dijkstrar@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:21:35 +0200 Subject: [PATCH 01/11] update docs for generators --- docs/generators/kotlin-misk.md | 1 + docs/generators/kotlin-server.md | 1 + docs/generators/kotlin-spring.md | 1 + docs/generators/kotlin.md | 1 + 4 files changed, 4 insertions(+) diff --git a/docs/generators/kotlin-misk.md b/docs/generators/kotlin-misk.md index 1fe3d8cd2db2..487d4a71637f 100644 --- a/docs/generators/kotlin-misk.md +++ b/docs/generators/kotlin-misk.md @@ -29,6 +29,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |artifactId|Generated artifact id (name of jar).| |null| |artifactVersion|Generated artifact's package version.| |1.0.0| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', 'original', and 'bestEffortBacktick' (like 'original' but tries to wrap values in backticks before falling back to sanitizing, e.g. `name,asc` stays `name,asc` rather than becoming nameCommaAsc; useful for sort/order enums)| |original| +|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.| |false| |generateStubImplClasses|Generate Stub Impl Classes| |false| |groupId|Generated artifact package's organization (i.e. maven groupId).| |org.openapitools| |implicitHeaders|Skip header parameters in the generated API methods.| |false| diff --git a/docs/generators/kotlin-server.md b/docs/generators/kotlin-server.md index 08125378d996..f555ed68d858 100644 --- a/docs/generators/kotlin-server.md +++ b/docs/generators/kotlin-server.md @@ -23,6 +23,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |artifactVersion|Generated artifact's package version.| |1.0.0| |delegatePattern|Whether to generate the server files using the delegate pattern. This option is currently supported only when using ktor library.| |false| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', 'original', and 'bestEffortBacktick' (like 'original' but tries to wrap values in backticks before falling back to sanitizing, e.g. `name,asc` stays `name,asc` rather than becoming nameCommaAsc; useful for sort/order enums)| |original| +|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.| |false| |featureAutoHead|Automatically provide responses to HEAD requests for existing routes that have the GET verb defined.| |true| |featureCORS|Ktor by default provides an interceptor for implementing proper support for Cross-Origin Resource Sharing (CORS). See enable-cors.org.| |false| |featureCompression|Adds ability to compress outgoing content using gzip, deflate or custom encoder and thus reduce size of the response.| |true| diff --git a/docs/generators/kotlin-spring.md b/docs/generators/kotlin-spring.md index 5eaa5ffc9cfa..d88bcf8f6291 100644 --- a/docs/generators/kotlin-spring.md +++ b/docs/generators/kotlin-spring.md @@ -32,6 +32,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |delegatePattern|Whether to generate the server files using the delegate pattern| |false| |documentationProvider|Select the OpenAPI documentation provider.|
**none**
Do not publish an OpenAPI specification.
**source**
Publish the original input OpenAPI specification.
**springdoc**
Generate an OpenAPI 3 specification using SpringDoc.
|springdoc| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', 'original', and 'bestEffortBacktick' (like 'original' but tries to wrap values in backticks before falling back to sanitizing, e.g. `name,asc` stays `name,asc` rather than becoming nameCommaAsc; useful for sort/order enums)| |original| +|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.| |false| |exceptionHandler|generate default global exception handlers (not compatible with reactive. enabling reactive will disable exceptionHandler )| |true| |generatePageableConstraintValidation|Generate a @ValidPageable annotation and PageableConstraintValidator class, and apply @ValidPageable to the injected Pageable parameter of operations whose 'page' or 'size' parameter specifies a maximum constraint. The annotation enforces those constraints on the Pageable object that replaces the individual page/size query parameters. Requires useBeanValidation=true and library=spring-boot.| |false| |generateSortValidation|Generate a @ValidSort annotation and SortValidator class, and apply @ValidSort to the injected Pageable parameter of operations whose 'sort' parameter has enum values. The annotation validates that sort values in the Pageable object match the allowed enum values from the spec. Requires useBeanValidation=true and library=spring-boot.| |false| diff --git a/docs/generators/kotlin.md b/docs/generators/kotlin.md index cbdab90fc3f8..e2533565f4e6 100644 --- a/docs/generators/kotlin.md +++ b/docs/generators/kotlin.md @@ -26,6 +26,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |dateLibrary|Option. Date library to use|
**threetenbp-localdatetime**
Threetenbp - Backport of JSR310 (jvm only, for legacy app only)
**kotlinx-datetime**
kotlinx-datetime (preferred for multiplatform)
**string**
String
**java8-localdatetime**
Java 8 native JSR310 (jvm only, for legacy app only)
**java8**
Java 8 native JSR310 (jvm only, preferred for jdk 1.8+)
**threetenbp**
Threetenbp - Backport of JSR310 (jvm only, preferred for jdk < 1.8)
|java8| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', 'original', and 'bestEffortBacktick' (like 'original' but tries to wrap values in backticks before falling back to sanitizing, e.g. `name,asc` stays `name,asc` rather than becoming nameCommaAsc; useful for sort/order enums)| |original| |explicitApi|Generates code with explicit access modifiers to comply with Kotlin Explicit API Mode.| |false| +|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.| |false| |failOnUnknownProperties|Fail Jackson de-serialization on unknown properties| |false| |generateOneOfAnyOfWrappers|Generate oneOf, anyOf schemas as wrappers. Only `jvm-retrofit2`(library) with `gson` or `kotlinx_serialization`(serializationLibrary) support this option.| |false| |generateRoomModels|Generate Android Room database models in addition to API models (JVM Volley library only)| |false| From b7a8641ab92cc3a79707745750ab8b0df3c764af Mon Sep 17 00:00:00 2001 From: dijkstrar <52031414+dijkstrar@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:48:04 +0200 Subject: [PATCH 02/11] only apply flag to kotlin spring generator --- docs/generators/kotlin-misk.md | 1 - docs/generators/kotlin-server.md | 1 - docs/generators/kotlin.md | 1 - .../codegen/languages/KotlinSpringServerCodegen.java | 7 +++++++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/generators/kotlin-misk.md b/docs/generators/kotlin-misk.md index 487d4a71637f..1fe3d8cd2db2 100644 --- a/docs/generators/kotlin-misk.md +++ b/docs/generators/kotlin-misk.md @@ -29,7 +29,6 @@ These options may be applied as additional-properties (cli) or configOptions (pl |artifactId|Generated artifact id (name of jar).| |null| |artifactVersion|Generated artifact's package version.| |1.0.0| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', 'original', and 'bestEffortBacktick' (like 'original' but tries to wrap values in backticks before falling back to sanitizing, e.g. `name,asc` stays `name,asc` rather than becoming nameCommaAsc; useful for sort/order enums)| |original| -|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.| |false| |generateStubImplClasses|Generate Stub Impl Classes| |false| |groupId|Generated artifact package's organization (i.e. maven groupId).| |org.openapitools| |implicitHeaders|Skip header parameters in the generated API methods.| |false| diff --git a/docs/generators/kotlin-server.md b/docs/generators/kotlin-server.md index f555ed68d858..08125378d996 100644 --- a/docs/generators/kotlin-server.md +++ b/docs/generators/kotlin-server.md @@ -23,7 +23,6 @@ These options may be applied as additional-properties (cli) or configOptions (pl |artifactVersion|Generated artifact's package version.| |1.0.0| |delegatePattern|Whether to generate the server files using the delegate pattern. This option is currently supported only when using ktor library.| |false| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', 'original', and 'bestEffortBacktick' (like 'original' but tries to wrap values in backticks before falling back to sanitizing, e.g. `name,asc` stays `name,asc` rather than becoming nameCommaAsc; useful for sort/order enums)| |original| -|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.| |false| |featureAutoHead|Automatically provide responses to HEAD requests for existing routes that have the GET verb defined.| |true| |featureCORS|Ktor by default provides an interceptor for implementing proper support for Cross-Origin Resource Sharing (CORS). See enable-cors.org.| |false| |featureCompression|Adds ability to compress outgoing content using gzip, deflate or custom encoder and thus reduce size of the response.| |true| diff --git a/docs/generators/kotlin.md b/docs/generators/kotlin.md index e2533565f4e6..cbdab90fc3f8 100644 --- a/docs/generators/kotlin.md +++ b/docs/generators/kotlin.md @@ -26,7 +26,6 @@ These options may be applied as additional-properties (cli) or configOptions (pl |dateLibrary|Option. Date library to use|
**threetenbp-localdatetime**
Threetenbp - Backport of JSR310 (jvm only, for legacy app only)
**kotlinx-datetime**
kotlinx-datetime (preferred for multiplatform)
**string**
String
**java8-localdatetime**
Java 8 native JSR310 (jvm only, for legacy app only)
**java8**
Java 8 native JSR310 (jvm only, preferred for jdk 1.8+)
**threetenbp**
Threetenbp - Backport of JSR310 (jvm only, preferred for jdk < 1.8)
|java8| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', 'original', and 'bestEffortBacktick' (like 'original' but tries to wrap values in backticks before falling back to sanitizing, e.g. `name,asc` stays `name,asc` rather than becoming nameCommaAsc; useful for sort/order enums)| |original| |explicitApi|Generates code with explicit access modifiers to comply with Kotlin Explicit API Mode.| |false| -|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.| |false| |failOnUnknownProperties|Fail Jackson de-serialization on unknown properties| |false| |generateOneOfAnyOfWrappers|Generate oneOf, anyOf schemas as wrappers. Only `jvm-retrofit2`(library) with `gson` or `kotlinx_serialization`(serializationLibrary) support this option.| |false| |generateRoomModels|Generate Android Room database models in addition to API models (JVM Volley library only)| |false| 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 dba07bae883e..735c38834574 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 @@ -311,6 +311,8 @@ public KotlinSpringServerCodegen() { addSwitch(COMPANION_OBJECT, "Whether to generate companion objects in data classes, enabling companion extensions.", companionObject); addSwitch(SUSPEND_FUNCTIONS, "Whether to generate suspend functions for API operations. Useful for Spring MVC with Kotlin coroutines without requiring the full reactive stack.", suspendFunctions); cliOptions.add(CliOption.newBoolean(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES_DESC, useDeductionForOneOfInterfaces)); + cliOptions.add(cliOption.newBoolean(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE_DESC).defaultValue("false")); + addSwitch(CodegenConstants.USE_ENUM_VALUE_INTERFACE, CodegenConstants.USE_ENUM_VALUE_INTERFACE_DESC, useEnumValueInterface); addSwitch(CodegenConstants.OPENAPI_NULLABLE, "Enable OpenAPI Jackson Nullable library (jackson-databind-nullable) for optional + nullable " @@ -659,6 +661,11 @@ public void processOpts() { } writePropertyBack(SKIP_DEFAULT_INTERFACE, skipDefaultInterface); + + if (additionalProperties.containsKey(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE)) { + setEnumUnknownDefaultCase(Boolean.parseBoolean(additionalProperties.get(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE).toString())); + } + if (additionalProperties.containsKey(REACTIVE)) { if (SPRING_CLOUD_LIBRARY.equals(library)) { throw new IllegalArgumentException("Currently, reactive option doesn't supported by Spring Cloud"); From cf86eb0f2ea52058f3aa52ee7c16e6ddd381ebae Mon Sep 17 00:00:00 2001 From: dijkstrar <52031414+dijkstrar@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:01:12 +0200 Subject: [PATCH 03/11] add unknown default value to moustache, and make tests to assert correctness --- .../kotlin-spring/enumClass.mustache | 31 +++-- .../spring/KotlinSpringServerCodegenTest.java | 112 ++++++++++++++++++ .../3_1/enum_unknown_default_case.yaml | 56 +++++++++ 3 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_1/enum_unknown_default_case.yaml diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/enumClass.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/enumClass.mustache index d22051e2367a..0ae38b03bb9d 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/enumClass.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/enumClass.mustache @@ -1,17 +1,32 @@ /** -* {{{description}}} -* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} -*/ -enum class {{classname}}(@get:JsonValue {{#useEnumValueInterface}}override {{/useEnumValueInterface}}val value: {{dataType}}) {{#vendorExtensions.x-kotlin-implements}}{{#-first}}: {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}} {{/vendorExtensions.x-kotlin-implements}}{ + * {{{description}}} + * Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} + */ +enum class {{classname}}( + @get:JsonValue + {{#useEnumValueInterface}}override {{/useEnumValueInterface}} + val value: {{dataType}} +) {{#vendorExtensions.x-kotlin-implements}}{{#-first}}: {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-kotlin-implements}} { + {{#allowableValues}}{{#enumVars}} - {{&name}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}; + {{&name}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} +{{#enumUnknownDefaultCase}}, + UNKNOWN_DEFAULT_OPEN_API({{#isString}}"unknown_default_open_api"{{/isString}}{{^isString}}{{#dataType}}null{{/dataType}}{{/isString}}) +{{/enumUnknownDefaultCase}}; companion object { @JvmStatic @JsonCreator fun forValue(value: {{dataType}}): {{classname}} { - return values().firstOrNull{it -> it.value == value} - ?: throw IllegalArgumentException("Unexpected value '$value' for enum '{{classname}}'") + return values().firstOrNull { it.value == value } +{{#enumUnknownDefaultCase}} + ?: UNKNOWN_DEFAULT_OPEN_API +{{/enumUnknownDefaultCase}} +{{^enumUnknownDefaultCase}} + ?: throw IllegalArgumentException( + "Unexpected value '$value' for enum '{{classname}}'" + ) +{{/enumUnknownDefaultCase}} } } -} +} \ No newline at end of file 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 4ac2f195bb74..1e82d46e5cd0 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 @@ -6678,4 +6678,116 @@ public void schemaMappingWithNullableAllOfRendersNullableKotlinProperty() throws String content = Files.readString(myObjectFile.toPath()); assertThat(content).contains("com.example.ExternalModel?"); } + + @Test(description = "test enumUnknownDefaultCase option") + public void testEnumUnknownDefaultCaseOption() { + final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + + // Test default value is false + codegen.processOpts(); + Assert.assertEquals(codegen.getEnumUnknownDefaultCase(), Boolean.FALSE); + + // Test setting via additionalProperties + codegen.additionalProperties().put(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, "true"); + codegen.processOpts(); + Assert.assertEquals(codegen.getEnumUnknownDefaultCase(), Boolean.TRUE); + } + + @Test(description = "test enum model generation with enumUnknownDefaultCase") + public void testEnumModelWithUnknownDefaultCase() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_1/enum_unknown_default_case.yaml"); + final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + + // Enable enumUnknownDefaultCase + codegen.additionalProperties().put(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, "true"); + codegen.setOpenAPI(openAPI); + codegen.processOpts(); + + // Verify that enumUnknownDefaultCase is set + Assert.assertEquals(codegen.getEnumUnknownDefaultCase(), Boolean.TRUE); + + // Process all models to trigger enum processing + Map schemas = openAPI.getComponents().getSchemas(); + Map allModels = new HashMap<>(); + for (String modelName : schemas.keySet()) { + Schema schema = schemas.get(modelName); + CodegenModel cm = codegen.fromModel(modelName, schema); + ModelsMap modelsMap = new ModelsMap(); + modelsMap.setModels(Collections.singletonList(new ModelMap(Collections.singletonMap("model", cm)))); + allModels.put(modelName, modelsMap); + } + + // Post-process to add enumVars + allModels = codegen.postProcessAllModels(allModels); + + // Get the ColorEnum model + CodegenModel colorEnum = null; + for (Map.Entry entry : allModels.entrySet()) { + if ("ColorEnum".equals(entry.getKey())) { + colorEnum = entry.getValue().getModels().get(0).getModel(); + break; + } + } + + Assert.assertNotNull(colorEnum); + Assert.assertNotNull(colorEnum.allowableValues); + + List> enumVars = (List>) colorEnum.allowableValues.get("enumVars"); + Assert.assertNotNull(enumVars); + + // Check that we have the expected enum values including UNKNOWN_DEFAULT_OPEN_API + Assert.assertTrue(enumVars.stream().anyMatch(var -> "'RED'".equals(var.get("value")))); + Assert.assertTrue(enumVars.stream().anyMatch(var -> "'GREEN'".equals(var.get("value")))); + Assert.assertTrue(enumVars.stream().anyMatch(var -> "'BLUE'".equals(var.get("value")))); + Assert.assertTrue(enumVars.stream().anyMatch(var -> "'YELLOW'".equals(var.get("value")))); + Assert.assertTrue(enumVars.stream().anyMatch(var -> "'unknown_default_open_api'".equals(var.get("value")))); + } + + @Test(description = "test enum generation with enumUnknownDefaultCase enabled") + public void testEnumGenerationWithUnknownDefaultCase() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("kotlin-spring") + .setInputSpec("src/test/resources/3_1/enum_unknown_default_case.yaml") + .setOutputDir(outputPath) + .addAdditionalProperty(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, "true"); + + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + Path enumFile = Paths.get(outputPath, "openapi_client", "models", "color_enum.kt"); + + TestUtils.assertFileContains(enumFile, + "UNKNOWN_DEFAULT_OPEN_API"); + + TestUtils.assertFileContains(enumFile, + "?: UNKNOWN_DEFAULT_OPEN_API"); +} + + @Test(description = "test enum generation with enumUnknownDefaultCase disabled") + public void testEnumGenerationWithoutUnknownDefaultCase() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("kotlin-spring") + .setInputSpec("src/test/resources/3_1/enum_unknown_default_case.yaml") + .setOutputDir(outputPath) + .addAdditionalProperty(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, "false"); + + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + Path enumFile = Paths.get(outputPath, "openapi_client", "models", "color_enum.kt"); + + // Check that UNKNOWN_DEFAULT_OPEN_API is NOT added + TestUtils.assertFileNotContains(enumFile, "UNKNOWN_DEFAULT_OPEN_API"); + + } } diff --git a/modules/openapi-generator/src/test/resources/3_1/enum_unknown_default_case.yaml b/modules/openapi-generator/src/test/resources/3_1/enum_unknown_default_case.yaml new file mode 100644 index 000000000000..05f23cbaef7b --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/enum_unknown_default_case.yaml @@ -0,0 +1,56 @@ +openapi: 3.0.0 +info: + title: Enum Test API + description: API for testing enum generation with enumUnknownDefaultCase + version: 1.0.0 +paths: + /colors: + get: + summary: Get color + operationId: getColor + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/ColorResponse' +components: + schemas: + ColorResponse: + type: object + required: + - color + - status + properties: + color: + $ref: '#/components/schemas/ColorEnum' + status: + $ref: '#/components/schemas/StatusEnum' + priority: + $ref: '#/components/schemas/PriorityEnum' + ColorEnum: + type: string + description: Available colors + enum: + - RED + - GREEN + - BLUE + - YELLOW + StatusEnum: + type: string + description: Status values + enum: + - PENDING + - APPROVED + - REJECTED + - IN_PROGRESS + PriorityEnum: + type: integer + description: Priority levels + enum: + - 1 + - 2 + - 3 + - 4 + - 5 \ No newline at end of file From c9a63370a908b248ae9b182ec7c173823193c357 Mon Sep 17 00:00:00 2001 From: dijkstrar <52031414+dijkstrar@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:25:54 +0200 Subject: [PATCH 04/11] address review comments --- .../spring/KotlinSpringServerCodegenTest.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) 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 1e82d46e5cd0..4c291bac7067 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 @@ -6759,7 +6759,15 @@ public void testEnumGenerationWithUnknownDefaultCase() throws IOException { List files = generator.opts(configurator.toClientOptInput()).generate(); files.forEach(File::deleteOnExit); - Path enumFile = Paths.get(outputPath, "openapi_client", "models", "color_enum.kt"); + Path enumFile = Paths.get(outputPath, + "src", + "main", + "kotlin", + "org", + "openapitools", + "model", + "Color.kt" + ); TestUtils.assertFileContains(enumFile, "UNKNOWN_DEFAULT_OPEN_API"); @@ -6784,7 +6792,15 @@ public void testEnumGenerationWithoutUnknownDefaultCase() throws IOException { List files = generator.opts(configurator.toClientOptInput()).generate(); files.forEach(File::deleteOnExit); - Path enumFile = Paths.get(outputPath, "openapi_client", "models", "color_enum.kt"); + Path enumFile = Paths.get(outputPath, + "src", + "main", + "kotlin", + "org", + "openapitools", + "model", + "Color.kt" + ); // Check that UNKNOWN_DEFAULT_OPEN_API is NOT added TestUtils.assertFileNotContains(enumFile, "UNKNOWN_DEFAULT_OPEN_API"); From 586b1714b86d9afef8944eef85be699135aac64f Mon Sep 17 00:00:00 2001 From: dijkstrar <52031414+dijkstrar@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:50:50 +0200 Subject: [PATCH 05/11] fix typo in cliOption to CliOption... --- .../codegen/languages/KotlinSpringServerCodegen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 735c38834574..4a9f319a15b9 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 @@ -311,7 +311,7 @@ public KotlinSpringServerCodegen() { addSwitch(COMPANION_OBJECT, "Whether to generate companion objects in data classes, enabling companion extensions.", companionObject); addSwitch(SUSPEND_FUNCTIONS, "Whether to generate suspend functions for API operations. Useful for Spring MVC with Kotlin coroutines without requiring the full reactive stack.", suspendFunctions); cliOptions.add(CliOption.newBoolean(CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES, CodegenConstants.USE_DEDUCTION_FOR_ONE_OF_INTERFACES_DESC, useDeductionForOneOfInterfaces)); - cliOptions.add(cliOption.newBoolean(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE_DESC).defaultValue("false")); + cliOptions.add(CliOption.newBoolean(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE_DESC).defaultValue("false")); addSwitch(CodegenConstants.USE_ENUM_VALUE_INTERFACE, CodegenConstants.USE_ENUM_VALUE_INTERFACE_DESC, useEnumValueInterface); addSwitch(CodegenConstants.OPENAPI_NULLABLE, From b899d9dc48db0322264819d096eef69547cc1231 Mon Sep 17 00:00:00 2001 From: dijkstrar <52031414+dijkstrar@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:58:12 +0200 Subject: [PATCH 06/11] oneline the override in the moustache file --- .../src/main/resources/kotlin-spring/enumClass.mustache | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/enumClass.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/enumClass.mustache index 0ae38b03bb9d..edc1be0587ce 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/enumClass.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/enumClass.mustache @@ -3,9 +3,7 @@ * Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} */ enum class {{classname}}( - @get:JsonValue - {{#useEnumValueInterface}}override {{/useEnumValueInterface}} - val value: {{dataType}} + @get:JsonValue {{#useEnumValueInterface}}override {{/useEnumValueInterface}}val value: {{dataType}} ) {{#vendorExtensions.x-kotlin-implements}}{{#-first}}: {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-kotlin-implements}} { {{#allowableValues}}{{#enumVars}} @@ -29,4 +27,4 @@ enum class {{classname}}( {{/enumUnknownDefaultCase}} } } -} \ No newline at end of file +} From 84be4bf5de972609541fc54a93c49d05ce636e9a Mon Sep 17 00:00:00 2001 From: dijkstrar <52031414+dijkstrar@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:35:12 +0200 Subject: [PATCH 07/11] add unknown default enum case to the dataclass moustache file --- .../src/main/resources/kotlin-spring/dataClass.mustache | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache index 3eeb19e70bcd..bc27ceed8e0f 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache @@ -50,14 +50,15 @@ */ enum class {{{nameInPascalCase}}}(@get:JsonValue {{#useEnumValueInterface}}{{^isContainer}}override {{/isContainer}}{{/useEnumValueInterface}}val value: {{#isContainer}}{{#items}}{{{dataType}}}{{/items}}{{/isContainer}}{{^isContainer}}{{{dataType}}}{{/isContainer}}) {{#vendorExtensions.x-kotlin-implements}}{{#-first}}: {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}} {{/vendorExtensions.x-kotlin-implements}}{ {{#allowableValues}}{{#enumVars}} - {{{name}}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}; + {{{name}}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}{{#enumUnknownDefaultCase}}, + UNKNOWN_DEFAULT_OPEN_API({{#isString}}"unknown_default_open_api"{{/isString}}{{^isString}}{{#dataType}}null{{/dataType}}{{/isString}}){{/enumUnknownDefaultCase}}; companion object { @JvmStatic @JsonCreator fun forValue(value: {{#isContainer}}{{#items}}{{{dataType}}}{{/items}}{{/isContainer}}{{^isContainer}}{{{dataType}}}{{/isContainer}}): {{{nameInPascalCase}}} { return values().firstOrNull{it -> it.value == value} - ?: throw IllegalArgumentException("Unexpected value '$value' for enum '{{nameInPascalCase}}'") + {{^enumUnknownDefaultCase}}?: throw IllegalArgumentException("Unexpected value '$value' for enum '{{nameInPascalCase}}'"){{/enumUnknownDefaultCase}}{{#enumUnknownDefaultCase}}?: UNKNOWN_DEFAULT_OPEN_API{{/enumUnknownDefaultCase}} } } } From 599444ea701ae588fef5dbd721d1f0a14703380e Mon Sep 17 00:00:00 2001 From: dijkstrar <52031414+dijkstrar@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:47:10 +0200 Subject: [PATCH 08/11] test the inclusion of the enum default for the dataclass files --- .../spring/KotlinSpringServerCodegenTest.java | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) 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 4c291bac7067..6b8cd87d7b1f 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 @@ -6806,4 +6806,126 @@ public void testEnumGenerationWithoutUnknownDefaultCase() throws IOException { TestUtils.assertFileNotContains(enumFile, "UNKNOWN_DEFAULT_OPEN_API"); } + + @Test(description = "test data class model generation containing inline enum with enumUnknownDefaultCase") + public void testDataClassModelWithUnknownDefaultCase() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_1/dataclass_unknown_default_case.yaml"); + final KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen(); + + // Enable enumUnknownDefaultCase + codegen.additionalProperties().put(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, "true"); + codegen.setOpenAPI(openAPI); + codegen.processOpts(); + + // Verify that enumUnknownDefaultCase is set + Assert.assertEquals(codegen.getEnumUnknownDefaultCase(), Boolean.TRUE); + + // Process all models to trigger enum processing + Map schemas = openAPI.getComponents().getSchemas(); + Map allModels = new HashMap<>(); + for (String modelName : schemas.keySet()) { + Schema schema = schemas.get(modelName); + CodegenModel cm = codegen.fromModel(modelName, schema); + ModelsMap modelsMap = new ModelsMap(); + modelsMap.setModels(Collections.singletonList(new ModelMap(Collections.singletonMap("model", cm)))); + allModels.put(modelName, modelsMap); + } + + // Post-process to add enumVars + allModels = codegen.postProcessAllModels(allModels); + + // Get the ColorResponse model + CodegenModel colorResponse = null; + for (Map.Entry entry : allModels.entrySet()) { + if ("ColorResponse".equals(entry.getKey())) { + colorResponse = entry.getValue().getModels().get(0).getModel(); + break; + } + } + + Assert.assertNotNull(colorResponse); + Assert.assertTrue(colorResponse.getHasEnums()); + + CodegenProperty colorVar = colorResponse.vars.stream() + .filter(var -> "color".equals(var.name)) + .findFirst() + .orElse(null); + + Assert.assertNotNull(colorVar); + Assert.assertNotNull(colorVar.allowableValues); + + List> enumVars = (List>) colorVar.allowableValues.get("enumVars"); + Assert.assertNotNull(enumVars); + + // Check that we have the expected inline enum values including UNKNOWN_DEFAULT_OPEN_API + Assert.assertTrue(enumVars.stream().anyMatch(var -> "'RED'".equals(var.get("value")))); + Assert.assertTrue(enumVars.stream().anyMatch(var -> "'GREEN'".equals(var.get("value")))); + Assert.assertTrue(enumVars.stream().anyMatch(var -> "'BLUE'".equals(var.get("value")))); + Assert.assertTrue(enumVars.stream().anyMatch(var -> "'YELLOW'".equals(var.get("value")))); + Assert.assertTrue(enumVars.stream().anyMatch(var -> "'unknown_default_open_api'".equals(var.get("value")))); + } + + @Test(description = "test data class generation containing inline enum with enumUnknownDefaultCase enabled") + public void testDataClassGenerationWithUnknownDefaultCase() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("kotlin-spring") + .setInputSpec("src/test/resources/3_1/dataclass_unknown_default_case.yaml") + .setOutputDir(outputPath) + .addAdditionalProperty(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, "true"); + + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + Path modelFile = Paths.get(outputPath, + "src", + "main", + "kotlin", + "org", + "openapitools", + "model", + "ColorResponse.kt" + ); + + // Assert inline Color enum includes UNKNOWN_DEFAULT_OPEN_API + TestUtils.assertFileContains(modelFile, + "UNKNOWN_DEFAULT_OPEN_API"); + + TestUtils.assertFileContains(modelFile, + "?: UNKNOWN_DEFAULT_OPEN_API"); + } + + @Test(description = "test data class generation containing inline enum with enumUnknownDefaultCase disabled") + public void testDataClassGenerationWithoutUnknownDefaultCase() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("kotlin-spring") + .setInputSpec("src/test/resources/3_1/dataclass_unknown_default_case.yaml") + .setOutputDir(outputPath) + .addAdditionalProperty(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, "false"); + + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(configurator.toClientOptInput()).generate(); + files.forEach(File::deleteOnExit); + + Path modelFile = Paths.get(outputPath, + "src", + "main", + "kotlin", + "org", + "openapitools", + "model", + "ColorResponse.kt" + ); + + // Check that UNKNOWN_DEFAULT_OPEN_API is NOT added within the inner enum of ColorResponse + TestUtils.assertFileNotContains(modelFile, "UNKNOWN_DEFAULT_OPEN_API"); + } } From e7702b203843307cd18cd803e7d4f0dea18522c4 Mon Sep 17 00:00:00 2001 From: dijkstrar <52031414+dijkstrar@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:49:02 +0200 Subject: [PATCH 09/11] create resources file for testing enum dataclass default --- .../3_1/dataclass_unknown_default_case.yaml | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 modules/openapi-generator/src/test/resources/3_1/dataclass_unknown_default_case.yaml diff --git a/modules/openapi-generator/src/test/resources/3_1/dataclass_unknown_default_case.yaml b/modules/openapi-generator/src/test/resources/3_1/dataclass_unknown_default_case.yaml new file mode 100644 index 000000000000..ce5711a2e043 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/dataclass_unknown_default_case.yaml @@ -0,0 +1,39 @@ +openapi: 3.0.0 +info: + title: Dataclass Nested Enum Test API + description: API for testing inline/nested enum generation in data classes with enumUnknownDefaultCase + version: 1.0.0 +paths: + /colors: + get: + summary: Get color + operationId: getColor + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/ColorResponse' +components: + schemas: + ColorResponse: + type: object + required: + - color + properties: + color: + type: string + description: Available colors + enum: + - RED + - GREEN + - BLUE + - YELLOW + priority: + type: integer + description: Priority levels + enum: + - 1 + - 2 + - 3 From df45fd0db0216f579d078ea18592bc0f5f6548ee Mon Sep 17 00:00:00 2001 From: dijkstrar <52031414+dijkstrar@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:55:02 +0200 Subject: [PATCH 10/11] fallback is non nullable, resolves to 11184809 --- .../src/main/resources/kotlin-spring/dataClass.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache index bc27ceed8e0f..36a725a378b9 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/dataClass.mustache @@ -51,7 +51,7 @@ enum class {{{nameInPascalCase}}}(@get:JsonValue {{#useEnumValueInterface}}{{^isContainer}}override {{/isContainer}}{{/useEnumValueInterface}}val value: {{#isContainer}}{{#items}}{{{dataType}}}{{/items}}{{/isContainer}}{{^isContainer}}{{{dataType}}}{{/isContainer}}) {{#vendorExtensions.x-kotlin-implements}}{{#-first}}: {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}} {{/vendorExtensions.x-kotlin-implements}}{ {{#allowableValues}}{{#enumVars}} {{{name}}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}{{#enumUnknownDefaultCase}}, - UNKNOWN_DEFAULT_OPEN_API({{#isString}}"unknown_default_open_api"{{/isString}}{{^isString}}{{#dataType}}null{{/dataType}}{{/isString}}){{/enumUnknownDefaultCase}}; + UNKNOWN_DEFAULT_OPEN_API({{#isString}}"unknown_default_open_api"{{/isString}}{{^isString}}{{#isLong}}11184809L{{/isLong}}{{#isInteger}}11184809{{/isInteger}}{{#isNumber}}11184809{{/isNumber}}{{#isDouble}}11184809.0{{/isDouble}}{{#isFloat}}11184809.0f{{/isFloat}}{{/isString}}){{/enumUnknownDefaultCase}}; companion object { @JvmStatic From 5f444ca5f95c0359b4e0e839d77d89424017da07 Mon Sep 17 00:00:00 2001 From: dijkstrar <52031414+dijkstrar@users.noreply.github.com> Date: Fri, 5 Jun 2026 11:39:18 +0200 Subject: [PATCH 11/11] update kotlin spring docs so that enumUnknownDefaultCase docs is equal to spring docs version --- docs/generators/kotlin-spring.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/generators/kotlin-spring.md b/docs/generators/kotlin-spring.md index d88bcf8f6291..714f5080f05d 100644 --- a/docs/generators/kotlin-spring.md +++ b/docs/generators/kotlin-spring.md @@ -32,7 +32,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |delegatePattern|Whether to generate the server files using the delegate pattern| |false| |documentationProvider|Select the OpenAPI documentation provider.|
**none**
Do not publish an OpenAPI specification.
**source**
Publish the original input OpenAPI specification.
**springdoc**
Generate an OpenAPI 3 specification using SpringDoc.
|springdoc| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', 'original', and 'bestEffortBacktick' (like 'original' but tries to wrap values in backticks before falling back to sanitizing, e.g. `name,asc` stays `name,asc` rather than becoming nameCommaAsc; useful for sort/order enums)| |original| -|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.| |false| +|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response. With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.|
**false**
No changes to the enums are made, this is the default option.
**true**
With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the enum case sent by the server is not known by the client/spec, can safely be decoded to this case.
|false| |exceptionHandler|generate default global exception handlers (not compatible with reactive. enabling reactive will disable exceptionHandler )| |true| |generatePageableConstraintValidation|Generate a @ValidPageable annotation and PageableConstraintValidator class, and apply @ValidPageable to the injected Pageable parameter of operations whose 'page' or 'size' parameter specifies a maximum constraint. The annotation enforces those constraints on the Pageable object that replaces the individual page/size query parameters. Requires useBeanValidation=true and library=spring-boot.| |false| |generateSortValidation|Generate a @ValidSort annotation and SortValidator class, and apply @ValidSort to the injected Pageable parameter of operations whose 'sort' parameter has enum values. The annotation validates that sort values in the Pageable object match the allowed enum values from the spec. Requires useBeanValidation=true and library=spring-boot.| |false|