Skip to content

[kotlin-client] Add oneOf discriminator support for multiplatform#23309

Draft
olsavmic wants to merge 2 commits intoOpenAPITools:masterfrom
olsavmic:feat/kotlin-multiplatform-oneof-discriminator
Draft

[kotlin-client] Add oneOf discriminator support for multiplatform#23309
olsavmic wants to merge 2 commits intoOpenAPITools:masterfrom
olsavmic:feat/kotlin-multiplatform-oneof-discriminator

Conversation

@olsavmic
Copy link

@olsavmic olsavmic commented Mar 21, 2026

Summary

  • Adds proper polymorphism support for oneOf schemas with discriminators in the kotlin-multiplatform client library
  • Generates sealed class hierarchy with @JsonClassDiscriminator / @SerialName (kotlinx.serialization) instead of brute-force wrapper deserialization
  • Mirrors the approach from [kotlin-server] Add polymorphism, oneOf and allOf support #22610 (kotlin-server with Jackson) adapted for kotlinx.serialization
  • Opt-in via useOneOfDiscriminatorSealedClass: true — no breaking changes for existing users

Problem

When a oneOf schema has a discriminator (e.g., a Pet parent with Dog and Cat subtypes), the multiplatform generator produced a wrapper class with actualInstance: Any? that tries to deserialize against each type sequentially. This ignores the discriminator entirely, leading to fragile deserialization and no type-safe parent-child relationship.

Before (default behavior, unchanged)

// Pet.kt — brute-force wrapper, no type hierarchy
@Serializable(with = Pet.PetSerializer::class)
data class Pet(var actualInstance: Any? = null) {

    object PetSerializer : KSerializer<Pet> {
        override fun deserialize(decoder: Decoder): Pet {
            val jsonElement = jsonDecoder.decodeJsonElement()
            val errorMessages = mutableListOf<String>()

            try {
                val instance = jsonDecoder.json.decodeFromJsonElement<Dog>(jsonElement)
                return Pet(actualInstance = instance)
            } catch (e: Exception) {
                errorMessages.add("Failed to deserialize as Dog: ${e.message}")
            }
            try {
                val instance = jsonDecoder.json.decodeFromJsonElement<Cat>(jsonElement)
                return Pet(actualInstance = instance)
            } catch (e: Exception) {
                errorMessages.add("Failed to deserialize as Cat: ${e.message}")
            }

            throw SerializationException("Cannot deserialize Pet. Tried: ...")
        }
    }
}

// Dog.kt — standalone, no relationship to Pet
@Serializable
data class Dog(
    val petType: kotlin.String,  // discriminator leaked as regular property
    val breed: kotlin.String,
    val bark: kotlin.Boolean? = null
)

After (useOneOfDiscriminatorSealedClass: true)

// Pet.kt — sealed class with discriminator annotation
@Serializable
@OptIn(ExperimentalSerializationApi::class)
@JsonClassDiscriminator(discriminator = "petType")
sealed class Pet

// Dog.kt — extends Pet, discriminator handled by framework
@Serializable
@SerialName(value = "DOG")
data class Dog(
    val breed: kotlin.String,
    val bark: kotlin.Boolean? = null
) : Pet()

// Cat.kt — extends Pet, discriminator handled by framework
@Serializable
@SerialName(value = "CAT")
data class Cat(
    val color: kotlin.String,
    val indoor: kotlin.Boolean? = null
) : Pet()

Usage

# generator config
generatorName: kotlin
library: multiplatform
additionalProperties:
  useOneOfDiscriminatorSealedClass: true

Solution

New config option:

  • useOneOfDiscriminatorSealedClass (boolean, default: false) — opt-in to generate sealed class hierarchies for oneOf schemas with discriminators in multiplatform

Codegen changes (KotlinClientCodegen.java):

  • In postProcessAllModels(), when useOneOfDiscriminatorSealedClass is enabled, detects multiplatform oneOf models with discriminators
  • Sets parent-child relationships (setParent, setParentModel) on child models
  • Sets discriminatorValue on children for @SerialName annotation
  • Removes discriminator properties from parent and children (handled by @JsonClassDiscriminator)
  • Clears merged properties from oneOf parent (they belong to children only)
  • Sets x-oneof-sealed-class vendor extension for template rendering

Template changes (multiplatform/oneof_class.mustache):

  • When x-oneof-sealed-class vendor extension is set: generates sealed class with @JsonClassDiscriminator
  • Otherwise: preserves existing brute-force wrapper behavior

Test plan

  • New unit test multiplatformOneOfDiscriminatorSealedClass verifies sealed class generation, @JsonClassDiscriminator, @SerialName, parent extension, and discriminator property removal
  • All 45 existing KotlinClientCodegenModelTest tests pass (no regressions)
  • Existing allOf-discriminator sample regenerates identically (no regressions)
  • New sample project kotlin-multiplatform-oneOf-discriminator generated and included

🤖 Generated with Claude Code

…rary

When a oneOf schema has a discriminator, the kotlin-multiplatform generator
now produces a sealed class hierarchy with @JsonClassDiscriminator instead
of a brute-force wrapper that tries each type sequentially. This mirrors
the approach from OpenAPITools#22610 (kotlin-server) adapted for kotlinx.serialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

5 issues found across 40 files

Prompt for AI agents (unresolved 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="samples/client/petstore/kotlin-multiplatform-oneOf-discriminator/src/commonMain/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">

<violation number="1" location="samples/client/petstore/kotlin-multiplatform-oneOf-discriminator/src/commonMain/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:23">
P2: Public primary constructor allows creating ApiClient without initializing `client`, leading to runtime crash when `request` is called.</violation>
</file>

<file name="samples/client/petstore/kotlin-multiplatform-oneOf-discriminator/docs/PetApi.md">

<violation number="1" location="samples/client/petstore/kotlin-multiplatform-oneOf-discriminator/docs/PetApi.md:23">
P2: Kotlin example is syntactically invalid: a `kotlin.String` value is assigned without quotes, causing compile failure.</violation>
</file>

<file name="samples/client/petstore/kotlin-multiplatform-oneOf-discriminator/src/commonMain/kotlin/org/openapitools/client/apis/PetApi.kt">

<violation number="1" location="samples/client/petstore/kotlin-multiplatform-oneOf-discriminator/src/commonMain/kotlin/org/openapitools/client/apis/PetApi.kt:76">
P2: Path parameter is inserted without pre-encoding, allowing `/` and reserved chars to alter route structure.</violation>
</file>

<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java:1022">
P2: Multiplatform oneOf discriminator handling unconditionally overwrites child parent metadata, breaking reused child models across multiple parents.</violation>
</file>

<file name="samples/client/petstore/kotlin-multiplatform-oneOf-discriminator/src/commonMain/kotlin/org/openapitools/client/infrastructure/RequestConfig.kt">

<violation number="1" location="samples/client/petstore/kotlin-multiplatform-oneOf-discriminator/src/commonMain/kotlin/org/openapitools/client/infrastructure/RequestConfig.kt:18">
P2: RequestConfig contract says body is excluded for caching, but implementation adds `body`, creating a misleading and contradictory API contract.</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.


import org.openapitools.client.auth.*

open class ApiClient(
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 21, 2026

Choose a reason for hiding this comment

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

P2: Public primary constructor allows creating ApiClient without initializing client, leading to runtime crash when request is called.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/kotlin-multiplatform-oneOf-discriminator/src/commonMain/kotlin/org/openapitools/client/infrastructure/ApiClient.kt, line 23:

<comment>Public primary constructor allows creating ApiClient without initializing `client`, leading to runtime crash when `request` is called.</comment>

<file context>
@@ -0,0 +1,188 @@
+
+import org.openapitools.client.auth.*
+
+open class ApiClient(
+        private val baseUrl: String
+) {
</file context>
Fix with Cubic

//import org.openapitools.client.models.*

val apiInstance = PetApi()
val id : kotlin.String = 38400000-8cf0-11bd-b23e-10b96e4ef00d // kotlin.String |
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 21, 2026

Choose a reason for hiding this comment

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

P2: Kotlin example is syntactically invalid: a kotlin.String value is assigned without quotes, causing compile failure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/kotlin-multiplatform-oneOf-discriminator/docs/PetApi.md, line 23:

<comment>Kotlin example is syntactically invalid: a `kotlin.String` value is assigned without quotes, causing compile failure.</comment>

<file context>
@@ -0,0 +1,53 @@
+//import org.openapitools.client.models.*
+
+val apiInstance = PetApi()
+val id : kotlin.String = 38400000-8cf0-11bd-b23e-10b96e4ef00d // kotlin.String | 
+try {
+    val result : Pet = apiInstance.getPet(id)
</file context>
Suggested change
val id : kotlin.String = 38400000-8cf0-11bd-b23e-10b96e4ef00d // kotlin.String |
val id : kotlin.String = "38400000-8cf0-11bd-b23e-10b96e4ef00d" // kotlin.String |
Fix with Cubic


val localVariableConfig = RequestConfig<kotlin.Any?>(
RequestMethod.GET,
"/v1/pet/{id}".replace("{" + "id" + "}", "$id"),
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 21, 2026

Choose a reason for hiding this comment

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

P2: Path parameter is inserted without pre-encoding, allowing / and reserved chars to alter route structure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/kotlin-multiplatform-oneOf-discriminator/src/commonMain/kotlin/org/openapitools/client/apis/PetApi.kt, line 76:

<comment>Path parameter is inserted without pre-encoding, allowing `/` and reserved chars to alter route structure.</comment>

<file context>
@@ -0,0 +1,90 @@
+
+        val localVariableConfig = RequestConfig<kotlin.Any?>(
+            RequestMethod.GET,
+            "/v1/pet/{id}".replace("{" + "id" + "}", "$id"),
+            query = localVariableQuery,
+            headers = localVariableHeaders,
</file context>
Fix with Cubic

val params: MutableMap<String, Any> = mutableMapOf(),
val query: MutableMap<String, List<String>> = mutableMapOf(),
val requiresAuthentication: Boolean,
val body: T? = null
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 21, 2026

Choose a reason for hiding this comment

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

P2: RequestConfig contract says body is excluded for caching, but implementation adds body, creating a misleading and contradictory API contract.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/kotlin-multiplatform-oneOf-discriminator/src/commonMain/kotlin/org/openapitools/client/infrastructure/RequestConfig.kt, line 18:

<comment>RequestConfig contract says body is excluded for caching, but implementation adds `body`, creating a misleading and contradictory API contract.</comment>

<file context>
@@ -0,0 +1,19 @@
+    val params: MutableMap<String, Any> = mutableMapOf(),
+    val query: MutableMap<String, List<String>> = mutableMapOf(),
+    val requiresAuthentication: Boolean,
+    val body: T? = null
+)
</file context>
Fix with Cubic

@olsavmic olsavmic marked this pull request as draft March 21, 2026 13:41
…rSealedClass

Adds a new boolean config option `useOneOfDiscriminatorSealedClass` (default: false)
that must be explicitly enabled. This avoids breaking existing multiplatform users
who rely on the current brute-force wrapper behavior for oneOf schemas.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

1 participant