Skip to content

Commit b1cbed1

Browse files
authored
[kotlin-client] Add decodeOrNull helper for Jackson enums, fix decode KDoc (#23791) (#23823)
Splits the two contracts that PR #22535 (7.18.0) conflated: - decode() stays strict and Jackson-bound (@JsonCreator entry point) - decodeOrNull() is added as a lenient counterpart for direct callers that prefer the pre-7.19.0 null-on-unknown behavior This gives users impacted by the 7.19.0 change a trivial migration path (MyEnum.decode(x) -> MyEnum.decodeOrNull(x)) without giving up the Jackson safety fix or matching the kotlin-spring/Java client behavior. Also corrects the KDoc on decode() which still claimed "null otherwise" even though the function now throws.
1 parent f3c54be commit b1cbed1

4 files changed

Lines changed: 84 additions & 3 deletions

File tree

modules/openapi-generator/src/main/resources/kotlin-client/enum_class.mustache

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ import kotlinx.serialization.encoding.Encoder
108108
*/
109109
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun encode(data: kotlin.Any?): kotlin.String? = if (data is {{classname}}) "$data" else null
110110

111+
{{^jackson}}
111112
/**
112113
* Returns a valid [{{classname}}] for [data], null otherwise.
113114
*/
114-
{{^jackson}}
115115
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun decode(data: kotlin.Any?): {{classname}}? = data?.let {
116116
val normalizedData = "$it".lowercase()
117117
entries.firstOrNull { value ->
@@ -120,6 +120,12 @@ import kotlinx.serialization.encoding.Encoder
120120
}
121121
{{/jackson}}
122122
{{#jackson}}
123+
/**
124+
* Returns a valid [{{classname}}] for [data].{{^isNullable}}{{^enumUnknownDefaultCase}}
125+
*
126+
* Throws [IllegalArgumentException] when [data] is null or does not match a known value.
127+
* For lenient lookup that returns null on unknown values, see [decodeOrNull].{{/enumUnknownDefaultCase}}{{/isNullable}}
128+
*/
123129
@JvmStatic
124130
@JsonCreator
125131
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun decode(data: kotlin.Any?): {{classname}}{{#isNullable}}?{{/isNullable}} {
@@ -138,6 +144,20 @@ import kotlinx.serialization.encoding.Encoder
138144
?: {{#allowableValues}}{{#enumVars}}{{#-last}}{{&name}}{{/-last}}{{/enumVars}}{{/allowableValues}}{{/enumUnknownDefaultCase}}{{^enumUnknownDefaultCase}}
139145
?: throw IllegalArgumentException("Unknown {{classname}} value: $data"){{/enumUnknownDefaultCase}}{{/isNullable}}
140146
}
147+
148+
/**
149+
* Returns a valid [{{classname}}] for [data], or null when [data] is null or does not match a known value.
150+
*
151+
* Lenient counterpart to [decode], intended for direct calls from Kotlin code.
152+
* Jackson deserialization uses [decode], which is strict.
153+
*/
154+
@JvmStatic
155+
{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}fun decodeOrNull(data: kotlin.Any?): {{classname}}? = data?.let {
156+
val normalizedData = "$it".lowercase()
157+
entries.firstOrNull { value ->
158+
it == value || normalizedData == "$value".lowercase()
159+
}
160+
}
141161
{{/jackson}}
142162
}
143163
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenModelTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,39 @@ public void testJacksonEnumsThrowForUnknownValue() throws IOException {
819819
TestUtils.assertFileContains(enumKt, "throw IllegalArgumentException(\"Unknown ExampleNumericEnum value: $data\")");
820820
}
821821

822+
@Test
823+
public void testJacksonEnumsExposeDecodeOrNullHelper() throws IOException {
824+
File output = Files.createTempDirectory("test").toFile();
825+
output.deleteOnExit();
826+
827+
final CodegenConfigurator configurator = new CodegenConfigurator()
828+
.setGeneratorName("kotlin")
829+
.setLibrary("jvm-retrofit2")
830+
.setAdditionalProperties(new HashMap<>() {{
831+
put(CodegenConstants.SERIALIZATION_LIBRARY, "jackson");
832+
put(CodegenConstants.MODEL_PACKAGE, "model");
833+
}})
834+
.setInputSpec("src/test/resources/3_0/kotlin/issue22534-kotlin-numeric-enum.yaml")
835+
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
836+
837+
final ClientOptInput clientOptInput = configurator.toClientOptInput();
838+
DefaultGenerator generator = new DefaultGenerator();
839+
840+
generator.opts(clientOptInput).generate();
841+
842+
final Path enumKt = Paths.get(output + "/src/main/kotlin/model/ExampleNumericEnum.kt");
843+
844+
// decodeOrNull should always be generated alongside decode in the Jackson branch
845+
// and must return null for unknown values without throwing.
846+
TestUtils.assertFileContains(enumKt, "fun decodeOrNull(data: kotlin.Any?): ExampleNumericEnum?");
847+
// decodeOrNull should not be annotated with @JsonCreator — only decode is the Jackson entry point.
848+
// Verify by checking @JsonCreator appears exactly once in the file.
849+
String content = new String(java.nio.file.Files.readAllBytes(enumKt), java.nio.charset.StandardCharsets.UTF_8);
850+
int jsonCreatorCount = content.split("@JsonCreator", -1).length - 1;
851+
Assert.assertEquals(jsonCreatorCount, 1,
852+
"Expected exactly one @JsonCreator annotation in the generated enum, found " + jsonCreatorCount);
853+
}
854+
822855
@Test
823856
public void testJacksonEnumsWithUnknownDefaultCase() throws IOException {
824857
File output = Files.createTempDirectory("test").toFile();

samples/client/echo_api/kotlin-jvm-spring-3-restclient/src/main/kotlin/org/openapitools/client/models/StringEnumRef.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ enum class StringEnumRef(@get:JsonValue val value: kotlin.String) {
6666
fun encode(data: kotlin.Any?): kotlin.String? = if (data is StringEnumRef) "$data" else null
6767

6868
/**
69-
* Returns a valid [StringEnumRef] for [data], null otherwise.
69+
* Returns a valid [StringEnumRef] for [data].
7070
*/
7171
@JvmStatic
7272
@JsonCreator
@@ -80,6 +80,20 @@ enum class StringEnumRef(@get:JsonValue val value: kotlin.String) {
8080
}
8181
?: unknown_default_open_api
8282
}
83+
84+
/**
85+
* Returns a valid [StringEnumRef] for [data], or null when [data] is null or does not match a known value.
86+
*
87+
* Lenient counterpart to [decode], intended for direct calls from Kotlin code.
88+
* Jackson deserialization uses [decode], which is strict.
89+
*/
90+
@JvmStatic
91+
fun decodeOrNull(data: kotlin.Any?): StringEnumRef? = data?.let {
92+
val normalizedData = "$it".lowercase()
93+
entries.firstOrNull { value ->
94+
it == value || normalizedData == "$value".lowercase()
95+
}
96+
}
8397
}
8498
}
8599

samples/client/echo_api/kotlin-jvm-spring-3-webclient/src/main/kotlin/org/openapitools/client/models/StringEnumRef.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ enum class StringEnumRef(@get:JsonValue val value: kotlin.String) {
6666
fun encode(data: kotlin.Any?): kotlin.String? = if (data is StringEnumRef) "$data" else null
6767

6868
/**
69-
* Returns a valid [StringEnumRef] for [data], null otherwise.
69+
* Returns a valid [StringEnumRef] for [data].
7070
*/
7171
@JvmStatic
7272
@JsonCreator
@@ -80,6 +80,20 @@ enum class StringEnumRef(@get:JsonValue val value: kotlin.String) {
8080
}
8181
?: unknown_default_open_api
8282
}
83+
84+
/**
85+
* Returns a valid [StringEnumRef] for [data], or null when [data] is null or does not match a known value.
86+
*
87+
* Lenient counterpart to [decode], intended for direct calls from Kotlin code.
88+
* Jackson deserialization uses [decode], which is strict.
89+
*/
90+
@JvmStatic
91+
fun decodeOrNull(data: kotlin.Any?): StringEnumRef? = data?.let {
92+
val normalizedData = "$it".lowercase()
93+
entries.firstOrNull { value ->
94+
it == value || normalizedData == "$value".lowercase()
95+
}
96+
}
8397
}
8498
}
8599

0 commit comments

Comments
 (0)