From 6a865d5b2ca257055e619d557dd28baeba9dc6cf Mon Sep 17 00:00:00 2001 From: Jeffrey Blayney Date: Mon, 21 Sep 2026 06:45:01 -0600 Subject: [PATCH 1/5] fix(kotlin): register enumUnknownDefaultCase as a CliOption enumUnknownDefaultCase has worked for the kotlin generator since it shipped in v5.3.1, but has never been advertised: it appears in neither `config-help -g kotlin` nor docs/generators/kotlin.md, so the only way to discover it is to read the generator source. Root cause: AbstractKotlinCodegen.java:308 calls cliOptions.clear() and re-adds only Kotlin-specific options, discarding the registration DefaultCodegen.java:2190 makes. DefaultCodegen.processOpts() still reads the key back from additionalProperties (DefaultCodegen.java:449), so the flag stayed functional while invisible. The clear() predates the flag by nearly four years (7cad47dd399, 2018-01-27 vs c305c717156, 2021-12-21), so Kotlin was never deliberately excluded. Registered on KotlinClientCodegen rather than AbstractKotlinCodegen because kotlin is the only Kotlin generator whose templates implement the fallback; kotlin-spring, kotlin-server, kotlin-vertx, kotlin-misk and kotlin-wiremock have zero references to it. Mirrors GoClientCodegen, which re-registers this same option after AbstractGoCodegen's clear(). docs/generators/kotlin.md regenerated with bin/utils/export_docs_generators.sh. No sample changes: the default is unchanged at false. Refs #12970 Co-Authored-By: Claude Opus 5 (1M context) --- docs/generators/kotlin.md | 1 + .../codegen/languages/KotlinClientCodegen.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/docs/generators/kotlin.md b/docs/generators/kotlin.md index a6268f931aa4..0e035db4fad2 100644 --- a/docs/generators/kotlin.md +++ b/docs/generators/kotlin.md @@ -25,6 +25,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |companionObject|Whether to generate companion objects in data classes, enabling companion extensions.| |false| |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| +|enumUnknownDefaultCase|Add an `unknown_default_open_api` enum case as a fallback for unrecognized values. Only `moshi`, `jackson` and `kotlinx_serialization`(serializationLibrary) decode unknown values to it; `gson` and `multiplatform` add the case but still fail on unknown values.|
**false**
No changes to the enums are made, this is the default option.
**true**
Each enum gains an `unknown_default_open_api` case that unrecognized values decode to.
|false| |explicitApi|Generates code with explicit access modifiers to comply with Kotlin Explicit API Mode.| |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| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java index 3898d9a9dc88..190d1e6240d5 100755 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java @@ -316,6 +316,20 @@ public KotlinClientCodegen() { cliOptions.add(CliOption.newBoolean(USE_RESPONSE_AS_RETURN_TYPE, "When using retrofit2 and coroutines, use `Response`<`T`> as return type instead of `T`.", true)); cliOptions.add(CliOption.newBoolean(USE_JACKSON_3, "Use Jackson 3 dependencies (tools.jackson package). Requires serializationLibrary=jackson. Incompatible with openApiNullable.")); + + // AbstractKotlinCodegen calls cliOptions.clear(), dropping DefaultCodegen's registration. + // Re-registered on kotlin alone: no other Kotlin generator's templates implement the fallback. + CliOption enumUnknownDefaultCaseOpt = CliOption.newBoolean( + CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, + "Add an `unknown_default_open_api` enum case as a fallback for unrecognized values. Only `moshi`, `jackson` and `kotlinx_serialization`(serializationLibrary) decode unknown values to it; `gson` and `multiplatform` add the case but still fail on unknown values.") + .defaultValue(Boolean.FALSE.toString()); + Map enumUnknownDefaultCaseOpts = new HashMap<>(); + enumUnknownDefaultCaseOpts.put("false", + "No changes to the enums are made, this is the default option."); + enumUnknownDefaultCaseOpts.put("true", + "Each enum gains an `unknown_default_open_api` case that unrecognized values decode to."); + enumUnknownDefaultCaseOpt.setEnum(enumUnknownDefaultCaseOpts); + cliOptions.add(enumUnknownDefaultCaseOpt); } @Override From 0f7d9520457350f4ac2c1f28ca79bcda08cbf78e Mon Sep 17 00:00:00 2001 From: Jeffrey Blayney Date: Mon, 21 Sep 2026 07:18:34 -0600 Subject: [PATCH 2/5] test(kotlin): guard the enumUnknownDefaultCase registration Adds a regression test asserting the option is present in KotlinClientCodegen.cliOptions(). The defect being fixed is an option silently disappearing from cliOptions, so without a test the same thing can happen again on the next refactor. Verified it discriminates: with the registration reverted it fails with "enumUnknownDefaultCase is not registered". Also shortens the `true` value description to "Each enum gains an `unknown_default_open_api` case." The previous wording promised that unrecognized values decode to it, which contradicts the option description directly above it - gson and multiplatform emit the case without decoding to it. Co-Authored-By: Claude Opus 5 (1M context) --- docs/generators/kotlin.md | 2 +- .../languages/KotlinClientCodegen.java | 2 +- .../kotlin/KotlinClientCodegenModelTest.java | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/generators/kotlin.md b/docs/generators/kotlin.md index 0e035db4fad2..00333984f6e1 100644 --- a/docs/generators/kotlin.md +++ b/docs/generators/kotlin.md @@ -25,7 +25,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |companionObject|Whether to generate companion objects in data classes, enabling companion extensions.| |false| |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| -|enumUnknownDefaultCase|Add an `unknown_default_open_api` enum case as a fallback for unrecognized values. Only `moshi`, `jackson` and `kotlinx_serialization`(serializationLibrary) decode unknown values to it; `gson` and `multiplatform` add the case but still fail on unknown values.|
**false**
No changes to the enums are made, this is the default option.
**true**
Each enum gains an `unknown_default_open_api` case that unrecognized values decode to.
|false| +|enumUnknownDefaultCase|Add an `unknown_default_open_api` enum case as a fallback for unrecognized values. Only `moshi`, `jackson` and `kotlinx_serialization`(serializationLibrary) decode unknown values to it; `gson` and `multiplatform` add the case but still fail on unknown values.|
**false**
No changes to the enums are made, this is the default option.
**true**
Each enum gains an `unknown_default_open_api` case.
|false| |explicitApi|Generates code with explicit access modifiers to comply with Kotlin Explicit API Mode.| |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| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java index 190d1e6240d5..0e8df430bae3 100755 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java @@ -327,7 +327,7 @@ public KotlinClientCodegen() { enumUnknownDefaultCaseOpts.put("false", "No changes to the enums are made, this is the default option."); enumUnknownDefaultCaseOpts.put("true", - "Each enum gains an `unknown_default_open_api` case that unrecognized values decode to."); + "Each enum gains an `unknown_default_open_api` case."); enumUnknownDefaultCaseOpt.setEnum(enumUnknownDefaultCaseOpts); cliOptions.add(enumUnknownDefaultCaseOpt); } 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 921cd035aeb4..e765ea8c6292 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 @@ -39,7 +39,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; @@ -1505,4 +1507,21 @@ public void testNoDefaultImplWhenNeitherSourceIsSet() throws IOException { Assert.assertTrue(sawJsonTypeInfo, "Expected at least one generated model with @JsonTypeInfo to exercise the code path"); } + + /** + * AbstractKotlinCodegen calls cliOptions.clear(), so an option inherited from DefaultCodegen stays + * functional while vanishing from config-help and docs/generators/kotlin.md. This guards against + * enumUnknownDefaultCase silently disappearing again. + */ + @Test + public void testEnumUnknownDefaultCaseIsRegisteredAsCliOption() { + CliOption option = new KotlinClientCodegen().cliOptions().stream() + .filter(o -> CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE.equals(o.getOpt())) + .findFirst() + .orElse(null); + + Assert.assertNotNull(option, CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE + " is not registered"); + Assert.assertEquals(option.getDefault(), "false"); + Assert.assertEquals(option.getEnum().keySet(), new HashSet<>(Arrays.asList("true", "false"))); + } } From 9137336223e6cf4ccdc0dd6344677043715d8b1e Mon Sep 17 00:00:00 2001 From: Jeffrey Blayney Date: Mon, 21 Sep 2026 07:20:33 -0600 Subject: [PATCH 3/5] refactor(kotlin): drop the redundant explicit default on the new option CliOption.newBoolean(opt, description) already delegates to the 3-arg overload with false, so .defaultValue(Boolean.FALSE.toString()) set what was already set. Matches this constructor's own convention: USE_JACKSON_3 and USE_NON_ASCII_HEADERS omit it, USE_RESPONSE_AS_RETURN_TYPE passes true explicitly because true is not the default. Docs output is unchanged. The regression test still asserts the default is false, so it now covers the implicit path too. Co-Authored-By: Claude Opus 5 (1M context) --- .../openapitools/codegen/languages/KotlinClientCodegen.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java index 0e8df430bae3..afa7d0e4e045 100755 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java @@ -321,8 +321,7 @@ public KotlinClientCodegen() { // Re-registered on kotlin alone: no other Kotlin generator's templates implement the fallback. CliOption enumUnknownDefaultCaseOpt = CliOption.newBoolean( CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, - "Add an `unknown_default_open_api` enum case as a fallback for unrecognized values. Only `moshi`, `jackson` and `kotlinx_serialization`(serializationLibrary) decode unknown values to it; `gson` and `multiplatform` add the case but still fail on unknown values.") - .defaultValue(Boolean.FALSE.toString()); + "Add an `unknown_default_open_api` enum case as a fallback for unrecognized values. Only `moshi`, `jackson` and `kotlinx_serialization`(serializationLibrary) decode unknown values to it; `gson` and `multiplatform` add the case but still fail on unknown values."); Map enumUnknownDefaultCaseOpts = new HashMap<>(); enumUnknownDefaultCaseOpts.put("false", "No changes to the enums are made, this is the default option."); From fd210645befc391bceec8a16f2c2ee1b2654b24b Mon Sep 17 00:00:00 2001 From: Jeffrey Blayney Date: Mon, 21 Sep 2026 07:27:11 -0600 Subject: [PATCH 4/5] docs(kotlin): make the enumUnknownDefaultCase caveat accurate per library The first wording claimed jackson and kotlinx_serialization decode unknown values to the fallback, and that gson and multiplatform "fail". Reading the templates, all three claims are wrong: - enum_class.mustache gates the jackson `decode()` fallback on {{^isNullable}}, so a nullable enum returns null. `decode()` carries @JsonCreator, so Jackson routes through it and never reaches the @JsonEnumDefaultValue / READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE path Serializer.kt configures. - The kotlinx_serialization KSerializer falls back only under {{#isString}}; {{^isString}} throws IllegalArgumentException, so integer enums still throw. - gson registers no enum adapter, so Gson's built-in returns null rather than failing - quieter than "fail" implies, and worse, since null is indistinguishable from an absent field. Also annotates `multiplatform` as (library) rather than leaving it inside a (serializationLibrary) clause - it is a library value, and line 300 of this file sets the convention of tagging each name with its own option. Test javadoc said the guard stops the option disappearing "again"; it never disappeared, it was never registered. Reworded. Co-Authored-By: Claude Opus 5 (1M context) --- docs/generators/kotlin.md | 2 +- .../openapitools/codegen/languages/KotlinClientCodegen.java | 2 +- .../codegen/kotlin/KotlinClientCodegenModelTest.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/generators/kotlin.md b/docs/generators/kotlin.md index 00333984f6e1..813ea5918b7a 100644 --- a/docs/generators/kotlin.md +++ b/docs/generators/kotlin.md @@ -25,7 +25,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |companionObject|Whether to generate companion objects in data classes, enabling companion extensions.| |false| |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| -|enumUnknownDefaultCase|Add an `unknown_default_open_api` enum case as a fallback for unrecognized values. Only `moshi`, `jackson` and `kotlinx_serialization`(serializationLibrary) decode unknown values to it; `gson` and `multiplatform` add the case but still fail on unknown values.|
**false**
No changes to the enums are made, this is the default option.
**true**
Each enum gains an `unknown_default_open_api` case.
|false| +|enumUnknownDefaultCase|Add an `unknown_default_open_api` enum case as a fallback for unrecognized values. Only `moshi`(serializationLibrary) decodes every unknown value to it: `jackson` skips nullable enums, `kotlinx_serialization` skips non-string enums, and neither `gson`(serializationLibrary) nor `multiplatform`(library) decodes to it at all.|
**false**
No changes to the enums are made, this is the default option.
**true**
Each enum gains an `unknown_default_open_api` case.
|false| |explicitApi|Generates code with explicit access modifiers to comply with Kotlin Explicit API Mode.| |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| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java index afa7d0e4e045..90b3c8ad9bcf 100755 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java @@ -321,7 +321,7 @@ public KotlinClientCodegen() { // Re-registered on kotlin alone: no other Kotlin generator's templates implement the fallback. CliOption enumUnknownDefaultCaseOpt = CliOption.newBoolean( CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, - "Add an `unknown_default_open_api` enum case as a fallback for unrecognized values. Only `moshi`, `jackson` and `kotlinx_serialization`(serializationLibrary) decode unknown values to it; `gson` and `multiplatform` add the case but still fail on unknown values."); + "Add an `unknown_default_open_api` enum case as a fallback for unrecognized values. Only `moshi`(serializationLibrary) decodes every unknown value to it: `jackson` skips nullable enums, `kotlinx_serialization` skips non-string enums, and neither `gson`(serializationLibrary) nor `multiplatform`(library) decodes to it at all."); Map enumUnknownDefaultCaseOpts = new HashMap<>(); enumUnknownDefaultCaseOpts.put("false", "No changes to the enums are made, this is the default option."); 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 e765ea8c6292..21ecfb924eea 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 @@ -1510,8 +1510,8 @@ public void testNoDefaultImplWhenNeitherSourceIsSet() throws IOException { /** * AbstractKotlinCodegen calls cliOptions.clear(), so an option inherited from DefaultCodegen stays - * functional while vanishing from config-help and docs/generators/kotlin.md. This guards against - * enumUnknownDefaultCase silently disappearing again. + * functional while vanishing from config-help and docs/generators/kotlin.md. That is how + * enumUnknownDefaultCase went undocumented for years; this guards the re-registration. */ @Test public void testEnumUnknownDefaultCaseIsRegisteredAsCliOption() { From fed2b668960954df9759ff55f8361ca53883682c Mon Sep 17 00:00:00 2001 From: Jeffrey Blayney Date: Mon, 21 Sep 2026 07:28:29 -0600 Subject: [PATCH 5/5] test(kotlin): use Set.of in the cliOption guard Set.of is already used across the test tree, including the kotlin package, and the target is Java 11. Drops the Arrays and HashSet imports. Co-Authored-By: Claude Opus 5 (1M context) --- .../codegen/kotlin/KotlinClientCodegenModelTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 21ecfb924eea..dad52b7ae72b 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 @@ -39,11 +39,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import static org.openapitools.codegen.CodegenConstants.*; import static org.openapitools.codegen.languages.KotlinClientCodegen.*; @@ -1522,6 +1521,6 @@ public void testEnumUnknownDefaultCaseIsRegisteredAsCliOption() { Assert.assertNotNull(option, CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE + " is not registered"); Assert.assertEquals(option.getDefault(), "false"); - Assert.assertEquals(option.getEnum().keySet(), new HashSet<>(Arrays.asList("true", "false"))); + Assert.assertEquals(option.getEnum().keySet(), Set.of("true", "false")); } }