Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d053a5b
update docs for generators
dijkstrar Jun 3, 2026
b7a8641
only apply flag to kotlin spring generator
dijkstrar Jun 3, 2026
cf86eb0
add unknown default value to moustache, and make tests to assert corr…
dijkstrar Jun 3, 2026
c9a6337
address review comments
dijkstrar Jun 3, 2026
586b171
fix typo in cliOption to CliOption...
dijkstrar Jun 4, 2026
b899d9d
oneline the override in the moustache file
dijkstrar Jun 4, 2026
84be4bf
add unknown default enum case to the dataclass moustache file
dijkstrar Jun 4, 2026
599444e
test the inclusion of the enum default for the dataclass files
dijkstrar Jun 4, 2026
e7702b2
create resources file for testing enum dataclass default
dijkstrar Jun 4, 2026
df45fd0
fallback is non nullable, resolves to 11184809
dijkstrar Jun 4, 2026
5f444ca
update kotlin spring docs so that enumUnknownDefaultCase docs is equa…
dijkstrar Jun 5, 2026
cabc59f
alter test assertions to only assert on UNKNOWN_DEFAULT_OPEN_API and …
dijkstrar Jun 7, 2026
a3bd32d
enum is correctly generated, but one of the cases uses incorrect capi…
dijkstrar Jun 9, 2026
8b2a43b
alter whitespace in enum class moustache, and generate dataclass mous…
dijkstrar Jun 9, 2026
cf4f29c
alter white space in moustache files, remove useless it->it.value==...
dijkstrar Jun 9, 2026
270695d
also make testcase for enum integers in kotlin spring :)
dijkstrar Jun 9, 2026
1cedbe5
tests now assert based on correctly, originally, capitalized unknown_…
dijkstrar Jun 9, 2026
929309e
test based on the lowercased value of unknown_default_open_api, and a…
dijkstrar Jun 10, 2026
99e545d
dont test on the enum vars themselves, but rely on the ci workflow fo…
dijkstrar Jun 10, 2026
316833e
update docs based on provided script
dijkstrar Jun 10, 2026
9e3518d
Merge branch 'master' into feature/add-kotlin-enumUnknownDefaultCase-…
dijkstrar Jun 26, 2026
dece211
add extra line after main merge...
dijkstrar Jun 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/configs/kotlin-spring-boot-integer-enum.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ additionalProperties:
annotationLibrary: none
documentationProvider: none
enumPropertyNaming: UPPERCASE
enumUnknownDefaultCase: "true"
useEnumValueInterface: "true"
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ additionalProperties:
useSwaggerUI: "false"
interfaceOnly: "true"
useSpringBoot3: "true"
enumUnknownDefaultCase: "true"
1 change: 1 addition & 0 deletions docs/generators/kotlin-spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.|<dl><dt>**none**</dt><dd>Do not publish an OpenAPI specification.</dd><dt>**source**</dt><dd>Publish the original input OpenAPI specification.</dd><dt>**springdoc**</dt><dd>Generate an OpenAPI 3 specification using SpringDoc.</dd></dl>|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|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,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 strict null handling. "
Expand Down Expand Up @@ -673,6 +675,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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,26 @@
* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}}
*/
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}};

{{#allowableValues}}{{#enumVars}}
{{&name}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}}{{/enumVars}}{{/allowableValues}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better avoid spaces in empty lines if possible.

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}}'")
return values().firstOrNull{ it.value == value }
{{#enumUnknownDefaultCase}}
{{#allowableValues}}
{{#enumVars}}
{{#-last}}
?: {{{name}}}
{{/-last}}
{{/enumVars}}
{{/allowableValues}}
{{/enumUnknownDefaultCase}}
{{^enumUnknownDefaultCase}}
?: throw IllegalArgumentException("Unexpected value '$value' for enum '{{nameInPascalCase}}'")

@wing328 wing328 Jul 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i prefer to keep the spacing (4-space) same as before (line 60)

{{/enumUnknownDefaultCase}}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
/**
* {{{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}};
* {{{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}}{{#-last}};{{/-last}}
{{/enumVars}}
{{/allowableValues}}

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}}
{{#allowableValues}}
{{#enumVars}}
{{#-last}}
?: {{&name}}
{{/-last}}
{{/enumVars}}
{{/allowableValues}}
{{/enumUnknownDefaultCase}}
{{^enumUnknownDefaultCase}}
?: throw IllegalArgumentException(
"Unexpected value '$value' for enum '{{classname}}'"
)
{{/enumUnknownDefaultCase}}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.parser.core.models.ParseOptions;
import org.apache.commons.io.FileUtils;
import org.assertj.core.api.Assertions;
import org.jetbrains.annotations.NotNull;
import org.openapitools.codegen.ClientOptInput;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.CodegenProperty;
import org.openapitools.codegen.DefaultGenerator;
import org.openapitools.codegen.TestUtils;
import org.openapitools.codegen.config.CodegenConfigurator;
Expand All @@ -23,6 +26,8 @@
import org.openapitools.codegen.languages.features.DocumentationProviderFeatures.AnnotationLibrary;
import org.openapitools.codegen.languages.features.DocumentationProviderFeatures.DocumentationProvider;
import org.openapitools.codegen.languages.features.SwaggerUIFeatures;
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.model.ModelMap;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
Expand Down Expand Up @@ -6778,6 +6783,99 @@ public void schemaMappingWithNullableAllOfRendersNullableKotlinProperty() throws
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 generation with enumUnknownDefaultCase enabled")
public void testEnumGenerationWithUnknownDefaultCase() throws IOException {
Map<String, File> files = generateFromContract(
"src/test/resources/3_1/enum_unknown_default_case.yaml",
Map.of(
CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, true
),
new HashMap<>(),
configurator -> {}
);

File colorFile = files.get("ColorEnum.kt");
assertThat(colorFile).isNotNull();

String content = Files.readString(colorFile.toPath());

assertThat(content).contains("unknown_default_open_api(\"unknown_default_open_api\")");
assertThat(content).contains("?: unknown_default_open_api");
}

@Test(description = "test enum generation with enumUnknownDefaultCase disabled")
public void testEnumGenerationWithoutUnknownDefaultCase() throws IOException {
Map<String, File> files = generateFromContract(
"src/test/resources/3_1/enum_unknown_default_case.yaml",
Map.of(
CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, false
),
new HashMap<>(),
configurator -> {}
);

File colorFile = files.get("ColorEnum.kt");
assertThat(colorFile).isNotNull();

String content = Files.readString(colorFile.toPath());

assertThat(content).doesNotContain("unknown_default_open_api(\"unknown_default_open_api\")");
assertThat(content).doesNotContain("?: unknown_default_open_api");
}

@Test(description = "test data class generation containing inline enum with enumUnknownDefaultCase enabled")
public void testDataClassGenerationWithUnknownDefaultCase() throws IOException {
Map<String, File> files = generateFromContract(
"src/test/resources/3_1/dataclass_unknown_default_case.yaml",
Map.of(
CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, true
),
new HashMap<>(),
configurator -> {}
);

File colorResponseFile = files.get("ColorResponse.kt");
assertThat(colorResponseFile).isNotNull();

String content = Files.readString(colorResponseFile.toPath());

assertThat(content).contains("unknown_default_open_api(\"unknown_default_open_api\")");
assertThat(content).contains("?: unknown_default_open_api");
}

@Test(description = "test data class generation containing inline enum with enumUnknownDefaultCase disabled")
public void testDataClassGenerationWithoutUnknownDefaultCase() throws IOException {
Map<String, File> files = generateFromContract(
"src/test/resources/3_1/dataclass_unknown_default_case.yaml",
Map.of(
CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, false
),
new HashMap<>(),
configurator -> {}
);

File colorResponseFile = files.get("ColorResponse.kt");
assertThat(colorResponseFile).isNotNull();

String content = Files.readString(colorResponseFile.toPath());

assertThat(content).doesNotContain("unknown_default_open_api(\"unknown_default_open_api\")");
assertThat(content).doesNotContain("?: unknown_default_open_api");

@Test(description = "nameMappings: @param:JsonProperty must use the original JSON field name for deserialization")
public void paramJsonPropertyAnnotationWithNameMappings() throws IOException {
// When a property is renamed via nameMappings, @param:JsonProperty must carry the
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,18 @@ data class Cat(
* Values: clueless,lazy,adventurous,aggressive
*/
enum class HuntingSkill(@get:JsonValue val value: kotlin.String) {

clueless("clueless"),
lazy("lazy"),
adventurous("adventurous"),
aggressive("aggressive");

companion object {
@JvmStatic
@JsonCreator
fun forValue(value: kotlin.String): HuntingSkill {
return values().firstOrNull{it -> it.value == value}
?: throw IllegalArgumentException("Unexpected value '$value' for enum 'HuntingSkill'")
return values().firstOrNull{ it.value == value }
?: throw IllegalArgumentException("Unexpected value '$value' for enum 'HuntingSkill'")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,20 @@ import jakarta.validation.constraints.Size
import jakarta.validation.Valid

/**
*
* Values: CAR,TRUCK
*/
*
* Values: CAR,TRUCK,unknown_default_open_api
*/
enum class VehicleType(@get:JsonValue val value: kotlin.String) {

CAR("CAR"),
TRUCK("TRUCK");
TRUCK("TRUCK"),
unknown_default_open_api("unknown_default_open_api");

companion object {
@JvmStatic
@JsonCreator
fun forValue(value: kotlin.String): VehicleType {
return values().firstOrNull{it -> it.value == value}
?: throw IllegalArgumentException("Unexpected value '$value' for enum 'VehicleType'")
return values().firstOrNull{ it.value == value }
?: unknown_default_open_api
}
}
}

Loading
Loading