Skip to content

[kotlin][client] Register enumUnknownDefaultCase as a CliOption so it appears in config-help and the docs - #24977

Merged
wing328 merged 5 commits into
OpenAPITools:masterfrom
thejeff77:fix/kotlin-register-enum-unknown-default-case-option
Sep 21, 2026
Merged

wing328 merged 5 commits into
OpenAPITools:masterfrom
thejeff77:fix/kotlin-register-enum-unknown-default-case-option

Conversation

@thejeff77

@thejeff77 thejeff77 commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

enumUnknownDefaultCase has worked for the kotlin generator since it shipped in v5.3.1, but it is advertised nowhere. It appears in neither config-help -g kotlin nor docs/generators/kotlin.md, so the only way to find out it exists is to read the generator source. In practice users conclude the kotlin client does not support enum fallbacks at all.

It is not a documentation oversight that can be fixed in the docs. docs/generators/*.md is generated from each generator's CliOption list by bin/utils/export_generator.sh (config-help --full-details --format markdown), and bin/utils/ensure-up-to-date re-runs it as a CI gate. The missing config-help entry and the missing docs row are the same bug, and registering the option fixes both.

Root cause

AbstractKotlinCodegen calls cliOptions.clear() at AbstractKotlinCodegen.java:308 and then re-adds only the Kotlin-specific options. That discards the registration DefaultCodegen makes at DefaultCodegen.java:2190-2199. DefaultCodegen.processOpts() still reads the key back out of additionalProperties at DefaultCodegen.java:449, which is why the flag has always been functional — just invisible. Passing --additional-properties=enumUnknownDefaultCase=true today produces a correct client; nothing tells you that you can.

The clear() predates the option by nearly four years — it was introduced in 7cad47dd399 (2018-01-27, "[kotlin-server] --library=ktor"), and enumUnknownDefaultCase landed in c305c717156 (2021-12-21, #11078). Kotlin was never deliberately excluded.

Why KotlinClientCodegen and not AbstractKotlinCodegen

kotlin is the only Kotlin generator whose templates implement the fallback. Grepping the template directories for enumUnknownDefaultCase / unknown_default_open_api gives 11 files under kotlin-client and zero under kotlin-spring, kotlin-server, kotlin-vertx-server, kotlin-misk, kotlin-wiremock and ktorm-schema.

Registering it on the abstract class would advertise it on generators where enabling it makes the output worse rather than better. The extra enum constant is injected language-neutrally in DefaultCodegen (injectEnumUnknownDefaultCase), so kotlin-spring with the flag on emits an unknown_default_open_api constant into the model's public API while forValue() still throws IllegalArgumentException on an unknown value — a phantom constant and the original crash.

This mirrors GoClientCodegen.java:167-177, which re-registers this same option after AbstractGoCodegen's own cliOptions.clear(); docs/generators/go.md has the row and the other Go generators do not.

Serialization-library support is partial, and the description says so

Support is partial in ways that are easy to get wrong, so the description names each gap rather than implying blanket support:

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.

For reference, where each stands in the kotlin-client templates today:

  • moshi — full. SerializerHelper.kt registers EnumJsonAdapter.create(...).withUnknownFallback(...) for every enum, wired in from Serializer.kt.mustache.
  • jacksonnon-nullable enums only. enum_class.mustache gates the decode() fallback on {{^isNullable}}; a nullable enum falls through to entries.firstOrNull { ... } and yields null. Because decode() carries @JsonCreator, Jackson routes through it and never reaches the @JsonEnumDefaultValue / READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE path that Serializer.kt.mustache sets up.
  • kotlinx_serializationstring enums only. The generated KSerializer emits the ?: <fallback> under {{#isString}} and ?: throw IllegalArgumentException("Unknown enum value: $value") under {{^isString}}, so an integer enum still throws. (Inline enums via data_class.mustache do fall back unconditionally.)
  • gson — none. No enum adapter is registered, so Gson's built-in returns null for an unrecognized name. Quieter than throwing, and arguably worse: null is indistinguishable from an absent field, and Gson bypasses Kotlin null checks.
  • multiplatform (a library, not a serializationLibrary) — none; the serializer throws.

The phrasing follows the neighbouring options in the same file, generateOneOfAnyOfWrappers and useJackson3, which scope themselves the same way.

Related

Scope and verification

One source file, one new test, and the regenerated docs/generators/kotlin.md (+1 line). No behaviour change: the default stays false and DefaultCodegen already initialised the field.

  • Docs regenerated with the repo tooling, not by hand. Ran all four scripts that bin/utils/ensure-up-to-date invokes (bin/meta-codegen.sh, bin/utils/export_docs_generators.sh, bin/utils/copy-to-website.sh, bin/utils/export_generators_readme.sh); export_docs_generators.sh regenerates every generator's doc and kotlin.md was the only one that moved.
  • Samples: regenerated all 128 bin/configs/kotlin*.yaml and bin/configs/ktorm*.yaml. git status --porcelain samples/ is empty.
  • config-help -g kotlin from a jar built on this branch now lists the option with the description and both enum values.
  • Added KotlinClientCodegenModelTest#testEnumUnknownDefaultCaseIsRegisteredAsCliOption, a regression guard for the registration itself. Verified it discriminates: with the registration reverted it fails with enumUnknownDefaultCase is not registered.
  • Tests: 1430 run, 0 failures — the whole org.openapitools.codegen.kotlin package (KotlinClientCodegenModelTest, KotlinClientCodegenApiTest, AbstractKotlinCodegenTest, the misk/server/spring/vertx tests) plus AllGeneratorsTest, which includes the noDuplicateCliOptions guard. Nothing asserts an exact option count, and KotlinMiskServerCodegenOptionsTest is unaffected because the registration is scoped to kotlin rather than the abstract class.

Note for maintainers

Five other DefaultCodegen options are dropped the same way for every Kotlin generator: ensureUniqueParams, allowUnicodeIdentifiers, prependFormOrBodyParameters, legacyDiscriminatorBehavior and disallowAdditionalPropertiesIfNotPresent. I have deliberately left those alone — each needs its own check of whether the Kotlin templates honour it, and a couple are behaviour-bearing. Happy to follow up separately if that is wanted.

PR checklist

  • Read the contribution guidelines.
  • Built the project and regenerated docs and samples. Specifically: ./mvnw clean package; all four scripts that bin/utils/ensure-up-to-date invokes (bin/meta-codegen.sh, bin/utils/export_docs_generators.sh, bin/utils/copy-to-website.sh, bin/utils/export_generators_readme.sh); and ./bin/generate-samples.sh over all 128 bin/configs/kotlin*.yaml and bin/configs/ktorm*.yaml. git status --porcelain samples/ is empty, and kotlin.md was the only generator doc that moved. I scoped sample regeneration to Kotlin rather than every language, since the change is confined to KotlinClientCodegen's constructor — happy to run the full sweep if CI disagrees.
  • @mentioning the Kotlin technical committee: @jimschubert, @andrewemery, @stefankoppier.

@jimschubert — flagging you specifically since 7cad47dd399 is where the cliOptions.clear() came from. Nothing about this change suggests that was wrong; the option simply did not exist yet, and nothing has re-added it since.

🤖 Generated with Claude Code


Summary by cubic

Registers the enumUnknownDefaultCase option for the kotlin client generator so it appears in config-help and docs/generators/kotlin.md. The flag has worked since v5.3.1 but was invisible because AbstractKotlinCodegen clears the option list, dropping DefaultCodegen's registration; no behavior changes, the default stays false.

  • Registration is scoped to KotlinClientCodegen, the only Kotlin generator whose templates implement the fallback.
  • Only moshi decodes every unknown value: jackson skips nullable enums, kotlinx_serialization skips non-string enums, and gson and multiplatform emit the case but don't decode to it.
  • A regression test guards the registration so the option cannot silently vanish again.
  • docs/generators/kotlin.md was regenerated with the repo tooling; no sample changes.

Written for commit fed2b66. Summary will update on new commits.

Review in cubic

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 (7cad47d, 2018-01-27 vs c305c71, 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 OpenAPITools#12970

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

thejeff77 and others added 4 commits September 21, 2026 07:18
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
…rary

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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@thejeff77

Copy link
Copy Markdown
Contributor Author

Ready for review — everything is green on fed2b66: all 11 GitHub Actions checks including Docs up-to-date, Samples up-to-date and Verify outputs, plus all four CircleCI nodes.

@wing328 — would this be considered for the 7.26.0 milestone (due 24 Sep)? It is small and low-risk: one CliOption registration in KotlinClientCodegen, one regression test, and the tool-regenerated docs/generators/kotlin.md row. No behaviour change — the default stays false, and DefaultCodegen already initialised the field, so nothing generates differently. git status --porcelain samples/ is empty, which the Samples up-to-date check confirms independently.

The user-visible effect is that enumUnknownDefaultCase stops being invisible for kotlin. It has worked since v5.3.1 but appears in neither config-help -g kotlin nor docs/generators/kotlin.md, because AbstractKotlinCodegen calls cliOptions.clear() and drops the inherited registration — a line that predates the option by nearly four years. Since docs/generators/*.md is generated from the CliOption list, the missing config-help entry and the missing docs row are one bug, and registering the option fixes both. Same shape as GoClientCodegen, which re-registers this same option after AbstractGoCodegen's own clear().

Happy to adjust anything — scope, wording of the description, or the test — whatever makes it easiest to take.

@wing328
wing328 merged commit 39d9183 into OpenAPITools:master Sep 21, 2026
15 checks passed
@wing328

wing328 commented Sep 21, 2026

Copy link
Copy Markdown
Member

just merged. thanks for the enhancement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants