From c97a63107909c9d9c7298306020fff7713f9f00b Mon Sep 17 00:00:00 2001 From: erenlmn Date: Sat, 10 Jan 2026 11:54:26 +0000 Subject: [PATCH] [kotlin-client] Fix enum @JsonCreator to throw for unknown values --- .../kotlin-client/enum_class.mustache | 26 ++++++++- .../kotlin/KotlinClientCodegenModelTest.java | 55 +++++++++++++++++++ .../client/models/StringEnumRef.kt | 12 ++-- .../client/models/StringEnumRef.kt | 12 ++-- 4 files changed, 94 insertions(+), 11 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/enum_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/enum_class.mustache index c34efdd961eb..3c2fdebf0b3d 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/enum_class.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/enum_class.mustache @@ -106,14 +106,34 @@ import kotlinx.serialization.* /** * Returns a valid [{{classname}}] for [data], null otherwise. */ - {{#jackson}}@JvmStatic - @JsonCreator - {{/jackson}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun decode(data: kotlin.Any?): {{classname}}? = data?.let { +{{^jackson}} + {{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun decode(data: kotlin.Any?): {{classname}}? = data?.let { val normalizedData = "$it".lowercase() values().firstOrNull { value -> it == value || normalizedData == "$value".lowercase() } } +{{/jackson}} +{{#jackson}} + @JvmStatic + @JsonCreator + {{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun decode(data: kotlin.Any?): {{classname}}{{#isNullable}}?{{/isNullable}} { + if (data == null) { +{{#isNullable}} + return null +{{/isNullable}} +{{^isNullable}} + throw IllegalArgumentException("Value for {{classname}} cannot be null") +{{/isNullable}} + } + val normalizedData = "$data".lowercase() + return values().firstOrNull { value -> + data == value || normalizedData == "$value".lowercase() + }{{#isNullable}}{{/isNullable}}{{^isNullable}}{{#enumUnknownDefaultCase}} + ?: {{#allowableValues}}{{#enumVars}}{{#-last}}{{&name}}{{/-last}}{{/enumVars}}{{/allowableValues}}{{/enumUnknownDefaultCase}}{{^enumUnknownDefaultCase}} + ?: throw IllegalArgumentException("Unknown {{classname}} value: $data"){{/enumUnknownDefaultCase}}{{/isNullable}} + } +{{/jackson}} } } {{#kotlinx_serialization}}{{#enumUnknownDefaultCase}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenModelTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenModelTest.java index a01c5e7b910d..60ae71fe5c72 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenModelTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenModelTest.java @@ -714,6 +714,61 @@ public void testJacksonEnumsUseJsonCreator() throws IOException { TestUtils.assertFileContains(enumKt, "@JsonCreator"); } + @Test + public void testJacksonEnumsThrowForUnknownValue() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("kotlin") + .setLibrary("jvm-retrofit2") + .setAdditionalProperties(new HashMap<>() {{ + put(CodegenConstants.SERIALIZATION_LIBRARY, "jackson"); + put(CodegenConstants.MODEL_PACKAGE, "model"); + }}) + .setInputSpec("src/test/resources/3_0/kotlin/issue22534-kotlin-numeric-enum.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + + generator.opts(clientOptInput).generate(); + + final Path enumKt = Paths.get(output + "/src/main/kotlin/model/ExampleNumericEnum.kt"); + + // Verify that the decode function throws IllegalArgumentException for unknown values + TestUtils.assertFileContains(enumKt, "throw IllegalArgumentException(\"Unknown ExampleNumericEnum value: $data\")"); + } + + @Test + public void testJacksonEnumsWithUnknownDefaultCase() throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("kotlin") + .setLibrary("jvm-retrofit2") + .setAdditionalProperties(new HashMap<>() {{ + put(CodegenConstants.SERIALIZATION_LIBRARY, "jackson"); + put(CodegenConstants.MODEL_PACKAGE, "model"); + put(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, "true"); + }}) + .setInputSpec("src/test/resources/3_0/kotlin/issue22534-kotlin-numeric-enum.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + + generator.opts(clientOptInput).generate(); + + final Path enumKt = Paths.get(output + "/src/main/kotlin/model/ExampleNumericEnum.kt"); + + // With enumUnknownDefaultCase=true, should return the default value instead of throwing + TestUtils.assertFileContains(enumKt, "@JsonEnumDefaultValue"); + // Should NOT contain throw for unknown values when enumUnknownDefaultCase is enabled + TestUtils.assertFileNotContains(enumKt, "throw IllegalArgumentException(\"Unknown ExampleNumericEnum value"); + } + @Test(description = "convert an empty model to object") public void emptyModelKotlinxSerializationTest() throws IOException { final Schema schema = new ObjectSchema() diff --git a/samples/client/echo_api/kotlin-jvm-spring-3-restclient/src/main/kotlin/org/openapitools/client/models/StringEnumRef.kt b/samples/client/echo_api/kotlin-jvm-spring-3-restclient/src/main/kotlin/org/openapitools/client/models/StringEnumRef.kt index 7f3601230336..3c149c23ea07 100644 --- a/samples/client/echo_api/kotlin-jvm-spring-3-restclient/src/main/kotlin/org/openapitools/client/models/StringEnumRef.kt +++ b/samples/client/echo_api/kotlin-jvm-spring-3-restclient/src/main/kotlin/org/openapitools/client/models/StringEnumRef.kt @@ -62,11 +62,15 @@ enum class StringEnumRef(@get:JsonValue val value: kotlin.String) { */ @JvmStatic @JsonCreator - fun decode(data: kotlin.Any?): StringEnumRef? = data?.let { - val normalizedData = "$it".lowercase() - values().firstOrNull { value -> - it == value || normalizedData == "$value".lowercase() + fun decode(data: kotlin.Any?): StringEnumRef { + if (data == null) { + throw IllegalArgumentException("Value for StringEnumRef cannot be null") } + val normalizedData = "$data".lowercase() + return values().firstOrNull { value -> + data == value || normalizedData == "$value".lowercase() + } + ?: unknown_default_open_api } } } diff --git a/samples/client/echo_api/kotlin-jvm-spring-3-webclient/src/main/kotlin/org/openapitools/client/models/StringEnumRef.kt b/samples/client/echo_api/kotlin-jvm-spring-3-webclient/src/main/kotlin/org/openapitools/client/models/StringEnumRef.kt index 7f3601230336..3c149c23ea07 100644 --- a/samples/client/echo_api/kotlin-jvm-spring-3-webclient/src/main/kotlin/org/openapitools/client/models/StringEnumRef.kt +++ b/samples/client/echo_api/kotlin-jvm-spring-3-webclient/src/main/kotlin/org/openapitools/client/models/StringEnumRef.kt @@ -62,11 +62,15 @@ enum class StringEnumRef(@get:JsonValue val value: kotlin.String) { */ @JvmStatic @JsonCreator - fun decode(data: kotlin.Any?): StringEnumRef? = data?.let { - val normalizedData = "$it".lowercase() - values().firstOrNull { value -> - it == value || normalizedData == "$value".lowercase() + fun decode(data: kotlin.Any?): StringEnumRef { + if (data == null) { + throw IllegalArgumentException("Value for StringEnumRef cannot be null") } + val normalizedData = "$data".lowercase() + return values().firstOrNull { value -> + data == value || normalizedData == "$value".lowercase() + } + ?: unknown_default_open_api } } }