Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,22 @@ import kotlinx.serialization.encoding.Encoder
{{/enumUnknownDefaultCase}}
{{^enumUnknownDefaultCase}}
{{#generateOneOfAnyOfWrappers}}
{{#discriminator}}
import kotlinx.serialization.KSerializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
{{/discriminator}}
{{/generateOneOfAnyOfWrappers}}
{{/enumUnknownDefaultCase}}
{{#generateOneOfAnyOfWrappers}}
{{#discriminator}}
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
{{#discriminator}}
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
{{/discriminator}}
Expand Down Expand Up @@ -100,13 +100,17 @@ import java.io.IOException
{{#discriminator}}
@Serializable(with = {{classname}}Serializer::class)
{{/discriminator}}
{{^discriminator}}
{{#serializableModel}}@KSerializable(with = {{classname}}.{{classname}}Serializer::class){{/serializableModel}}{{^serializableModel}}@Serializable(with = {{classname}}.{{classname}}Serializer::class){{/serializableModel}}
{{/discriminator}}
{{/generateOneOfAnyOfWrappers}}
{{/kotlinx_serialization}}
{{#isDeprecated}}
@Deprecated(message = "This schema is deprecated.")
{{/isDeprecated}}
{{>additionalModelTypeAnnotations}}
{{#kotlinx_serialization}}
{{#discriminator}}
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}sealed interface {{classname}} {
{{#discriminator.mappedModels}}
@JvmInline
Expand Down Expand Up @@ -150,6 +154,78 @@ import java.io.IOException
}
}
}
{{/discriminator}}
{{^discriminator}}
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}data class {{classname}}(var actualInstance: Any? = null) {

{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}object {{classname}}Serializer : KSerializer<{{classname}}> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("{{classname}}") {
element("type", JsonPrimitive.serializer().descriptor)
element("actualInstance", JsonElement.serializer().descriptor)
}

override fun serialize(encoder: Encoder, value: {{classname}}) {
val jsonEncoder = encoder as? JsonEncoder ?: throw SerializationException("{{classname}} can only be serialized with Json")

when (val instance = value.actualInstance) {
{{#composedSchemas}}
{{#oneOf}}
{{#isPrimitiveType}}
{{#isString}}
is kotlin.String -> jsonEncoder.encodeString(instance)
{{/isString}}
{{#isBoolean}}
is kotlin.Boolean -> jsonEncoder.encodeBoolean(instance)
{{/isBoolean}}
{{#isInteger}}
{{^isLong}}
is kotlin.Int -> jsonEncoder.encodeInt(instance)
{{/isLong}}
{{/isInteger}}
{{#isLong}}
is kotlin.Long -> jsonEncoder.encodeLong(instance)
{{/isLong}}
{{#isNumber}}
{{#isDouble}}
is kotlin.Double -> jsonEncoder.encodeDouble(instance)
{{/isDouble}}
{{#isFloat}}
is kotlin.Float -> jsonEncoder.encodeFloat(instance)
{{/isFloat}}
{{/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.

{{/isPrimitiveType}}
{{/oneOf}}
{{/composedSchemas}}
null -> jsonEncoder.encodeJsonElement(JsonNull)
else -> throw SerializationException("Unknown type in actualInstance: ${instance::class}")
}
}

override fun deserialize(decoder: Decoder): {{classname}} {
val jsonDecoder = decoder as? JsonDecoder ?: throw SerializationException("{{classname}} can only be deserialized with Json")
val jsonElement = jsonDecoder.decodeJsonElement()

val errorMessages = mutableListOf<String>()

{{#composedSchemas}}
{{#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.

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.

} catch (e: Exception) {
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
}
{{/oneOf}}
{{/composedSchemas}}

throw SerializationException("Cannot deserialize {{classname}}. Tried: ${errorMessages.joinToString(", ")}")
}
}
}
{{/discriminator}}
{{/kotlinx_serialization}}
{{^kotlinx_serialization}}
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}data class {{classname}}(var actualInstance: Any? = null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,40 @@ public void polymorphicKotlinxSerialization() throws IOException {
TestUtils.assertFileContains(birdKt, "@SerialName(value = \"BIRD\")");
}

@Test(description = "generate oneOf wrapper with primitive types using kotlinx_serialization")
public void oneOfPrimitiveKotlinxSerialization() throws IOException {
File output = Files.createTempDirectory("test").toFile();
output.deleteOnExit();

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("kotlin")
.setLibrary("jvm-retrofit2")
.setAdditionalProperties(new HashMap<>() {{
put(CodegenConstants.SERIALIZATION_LIBRARY, "kotlinx_serialization");
put("generateOneOfAnyOfWrappers", true);
}})
.setInputSpec("src/test/resources/3_0/issue_19942.json")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

final ClientOptInput clientOptInput = configurator.toClientOptInput();
DefaultGenerator generator = new DefaultGenerator();
generator.opts(clientOptInput).generate();

final Path oneOfModelKt = Paths.get(output + "/src/main/kotlin/org/openapitools/client/models/ObjectWithComplexOneOfId.kt");
// generates data class with actualInstance (not empty sealed interface)
TestUtils.assertFileContains(oneOfModelKt, "data class ObjectWithComplexOneOfId");
TestUtils.assertFileContains(oneOfModelKt, "var actualInstance: Any?");
// has a custom KSerializer
TestUtils.assertFileContains(oneOfModelKt, "object ObjectWithComplexOneOfIdSerializer : KSerializer<ObjectWithComplexOneOfId>");
// serializer handles primitive types
TestUtils.assertFileContains(oneOfModelKt, "is kotlin.String -> jsonEncoder.encodeString(instance)");
// serializer handles deserialization via try-each
TestUtils.assertFileContains(oneOfModelKt, "decodeFromJsonElement<kotlin.String>(jsonElement)");
// parent model references the oneOf wrapper type
final Path parentModelKt = Paths.get(output + "/src/main/kotlin/org/openapitools/client/models/ObjectWithComplexOneOf.kt");
TestUtils.assertFileContains(parentModelKt, "val id: ObjectWithComplexOneOfId?");
}

@Test(description = "generate polymorphic jackson model")
public void polymorphicJacksonSerialization() throws IOException {
File output = Files.createTempDirectory("test").toFile();
Expand Down