Skip to content

fix(kotlin): support oneOf with primitive types in kotlinx_serialization#22986

Open
pvcresin wants to merge 1 commit intoOpenAPITools:masterfrom
pvcresin:fix/kotlin-kotlinx-oneof-primitive-types
Open

fix(kotlin): support oneOf with primitive types in kotlinx_serialization#22986
pvcresin wants to merge 1 commit intoOpenAPITools:masterfrom
pvcresin:fix/kotlin-kotlinx-oneof-primitive-types

Conversation

@pvcresin
Copy link

@pvcresin pvcresin commented Feb 16, 2026

Description

The kotlin-client generator with jvm-retrofit2 library and kotlinx_serialization failed to generate working code for oneOf schemas containing primitive types (e.g. string, integer) without a discriminator. The existing template only handled discriminator-based oneOf via sealed interfaces, producing an empty class for primitive oneOf.

company_id:
  oneOf:
    - type: string
    - type: integer
      format: int64

Changes:

  • Add a non-discriminator fallback in oneof_class.mustache that generates a data class with actualInstance and a custom KSerializer, ported from the multiplatform template
  • Add isLong (int64) handling to the serialize branch, with an isInteger guard to avoid generating a dead branch for int64 types
  • Add test for oneOf primitive types with kotlinx_serialization

Before

@Serializable
class TestModelCompanyId () {
}

After

@Serializable(with = TestModelCompanyId.TestModelCompanyIdSerializer::class)
data class TestModelCompanyId(var actualInstance: Any? = null) {

    object TestModelCompanyIdSerializer : KSerializer<TestModelCompanyId> {
        override fun serialize(encoder: Encoder, value: TestModelCompanyId) {
            when (val instance = value.actualInstance) {
                is kotlin.String -> jsonEncoder.encodeString(instance)
                is kotlin.Long -> jsonEncoder.encodeLong(instance)
                null -> jsonEncoder.encodeJsonElement(JsonNull)
                else -> throw SerializationException(...)
            }
        }

        override fun deserialize(decoder: Decoder): TestModelCompanyId {
            // try each type in schema order
            try { decodeFromJsonElement<kotlin.String>(...) } catch { ... }
            try { decodeFromJsonElement<kotlin.Long>(...) } catch { ... }
        }
    }
}

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • File the PR against the correct branch: master (upcoming 7.x.0 minor release - breaking changes with fallbacks), 8.0.x (breaking changes without fallbacks)
  • If your PR solves a reported issue, reference it using GitHub's linking syntax (e.g., having "fixes #123" present in the PR description) (no existing issue)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request. (cc: @karismann @Zomzog @andrewemery @4brunu @yutaka0m @stefankoppier @e5l @dennisameling)

Summary by cubic

Fixes Kotlin client generation with jvm-retrofit2 and kotlinx_serialization for oneOf schemas containing primitive types without a discriminator. The generator now produces a data class wrapper with a custom KSerializer so these models serialize and deserialize correctly.

  • Bug Fixes
    • Added non-discriminator fallback in oneof_class.mustache: generate a data class with actualInstance and a custom KSerializer (ported from multiplatform).
    • Added int64 (isLong) handling in serialization; guarded with isInteger to avoid dead branches.
    • Added a test that verifies primitive oneOf wrapper generation and serializer logic with kotlinx_serialization.

Written for commit 2de0665. Summary will update on new commits.

The kotlin-client generator with `jvm-retrofit2` library and
`kotlinx_serialization` failed to generate working code for oneOf
schemas containing primitive types (e.g. string, integer) without a
discriminator. The existing template only handled discriminator-based
oneOf via sealed interfaces, producing an empty class for primitive
oneOf.

Changes:
- Add a non-discriminator fallback in oneof_class.mustache that
  generates a data class with actualInstance and a custom KSerializer,
  ported from the multiplatform template
- Add isLong (int64) handling to the serialize branch, with an
  isInteger guard to avoid generating a dead branch for int64 types
- Add test for oneOf primitive types with kotlinx_serialization
Copy link
Contributor

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

Choose a reason for hiding this comment

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

1 issue found across 2 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="modules/openapi-generator/src/main/resources/kotlin-client/oneof_class.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-client/oneof_class.mustache:217">
P2: oneOf deserialization returns on the first successful decode without checking for multiple matches, so ambiguous primitives (e.g., Int/Long/Double) will silently select the first schema instead of enforcing exactly one match.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

{{#oneOf}}
try {
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
return {{classname}}(actualInstance = instance)
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 16, 2026

Choose a reason for hiding this comment

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

P2: oneOf deserialization returns on the first successful decode without checking for multiple matches, so ambiguous primitives (e.g., Int/Long/Double) will silently select the first schema instead of enforcing exactly one match.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/kotlin-client/oneof_class.mustache, line 217:

<comment>oneOf deserialization returns on the first successful decode without checking for multiple matches, so ambiguous primitives (e.g., Int/Long/Double) will silently select the first schema instead of enforcing exactly one match.</comment>

<file context>
@@ -150,6 +154,78 @@ import java.io.IOException
+            {{#oneOf}}
+            try {
+                val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
+                return {{classname}}(actualInstance = instance)
+            } catch (e: Exception) {
+                errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
</file context>
Fix with Cubic

Copy link
Author

@pvcresin pvcresin Feb 16, 2026

Choose a reason for hiding this comment

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

This is consistent with the existing multiplatform template (libraries/multiplatform/oneof_class.mustache L64-72) which uses the same try-each approach.
The oneOf semantics require schemas to be mutually exclusive, so the first-match behavior is by design.

@pvcresin
Copy link
Author

@cubic-dev-ai rerun a review

@cubic-dev-ai
Copy link
Contributor

cubic-dev-ai bot commented Feb 17, 2026

@cubic-dev-ai rerun a review

@pvcresin I have started the AI code review. It will take a few minutes to complete.

Copy link
Contributor

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

Choose a reason for hiding this comment

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

1 issue found across 2 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="modules/openapi-generator/src/main/resources/kotlin-client/oneof_class.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-client/oneof_class.mustache:198">
P2: `{{{dataType}}}.serializer()` will not compile for parameterized/collection oneOf types (e.g., `List<Foo>`/`Map<String,Bar>`), so the generated Kotlin for oneOf with array/map schemas will fail to compile. Use a collection serializer or the top‑level `serializer<T>()` instead.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

{{/isNumber}}
{{/isPrimitiveType}}
{{^isPrimitiveType}}
is {{{dataType}}} -> jsonEncoder.encodeSerializableValue({{{dataType}}}.serializer(), instance)
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 17, 2026

Choose a reason for hiding this comment

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

P2: {{{dataType}}}.serializer() will not compile for parameterized/collection oneOf types (e.g., List<Foo>/Map<String,Bar>), so the generated Kotlin for oneOf with array/map schemas will fail to compile. Use a collection serializer or the top‑level serializer<T>() instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/kotlin-client/oneof_class.mustache, line 198:

<comment>`{{{dataType}}}.serializer()` will not compile for parameterized/collection oneOf types (e.g., `List<Foo>`/`Map<String,Bar>`), so the generated Kotlin for oneOf with array/map schemas will fail to compile. Use a collection serializer or the top‑level `serializer<T>()` instead.</comment>

<file context>
@@ -150,6 +154,78 @@ import java.io.IOException
+                {{/isNumber}}
+                {{/isPrimitiveType}}
+                {{^isPrimitiveType}}
+                is {{{dataType}}} -> jsonEncoder.encodeSerializableValue({{{dataType}}}.serializer(), instance)
+                {{/isPrimitiveType}}
+                {{/oneOf}}
</file context>
Fix with Cubic

Copy link
Author

Choose a reason for hiding this comment

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

This line is ported directly from the existing multiplatform template (libraries/multiplatform/oneof_class.mustache L49), which uses the same pattern.

This PR focuses on primitive type oneOf support.
Collection/parameterized type handling in oneOf is a broader issue that affects the multiplatform template as well and should be addressed separately.

@pvcresin
Copy link
Author

@cubic-dev-ai rerun a review

@cubic-dev-ai
Copy link
Contributor

cubic-dev-ai bot commented Feb 17, 2026

@cubic-dev-ai rerun a review

@pvcresin I have started the AI code review. It will take a few minutes to complete.

Copy link
Contributor

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

Choose a reason for hiding this comment

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

1 issue found across 2 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="modules/openapi-generator/src/main/resources/kotlin-client/oneof_class.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-client/oneof_class.mustache:217">
P2: Non-discriminator oneOf deserialization returns on the first successful decode, so overlapping primitive schemas can match multiple types but the first one wins, violating oneOf’s “exactly one” semantics.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

{{#oneOf}}
try {
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
return {{classname}}(actualInstance = instance)
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 17, 2026

Choose a reason for hiding this comment

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

P2: Non-discriminator oneOf deserialization returns on the first successful decode, so overlapping primitive schemas can match multiple types but the first one wins, violating oneOf’s “exactly one” semantics.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/kotlin-client/oneof_class.mustache, line 217:

<comment>Non-discriminator oneOf deserialization returns on the first successful decode, so overlapping primitive schemas can match multiple types but the first one wins, violating oneOf’s “exactly one” semantics.</comment>

<file context>
@@ -150,6 +154,78 @@ import java.io.IOException
+            {{#oneOf}}
+            try {
+                val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
+                return {{classname}}(actualInstance = instance)
+            } catch (e: Exception) {
+                errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
</file context>
Fix with Cubic

Copy link
Author

Choose a reason for hiding this comment

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

#22986 (comment)

Same as the previous comment — this is the same try-each approach used in the multiplatform template (libraries/multiplatform/oneof_class.mustache L64-72).

oneOf semantics require the schemas to be mutually exclusive, so the first-match behavior is by design.

@pvcresin
Copy link
Author

This is a PR regarding Kotlin. Could you please review it?

@karismann @Zomzog @andrewemery @4brunu @yutaka0m @stefankoppier @e5l @dennisameling

Copy link
Contributor

@e5l e5l left a comment

Choose a reason for hiding this comment

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

Hey @pvcresin, thank you for the PR!

lgtm

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants