Skip to content

Commit 7296d21

Browse files
committed
fix(kotlin): support oneOf with primitive types in kotlinx_serialization
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
1 parent 3252fcf commit 7296d21

2 files changed

Lines changed: 114 additions & 4 deletions

File tree

modules/openapi-generator/src/main/resources/kotlin-client/oneof_class.mustache

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,22 @@ import kotlinx.serialization.encoding.Encoder
3939
{{/enumUnknownDefaultCase}}
4040
{{^enumUnknownDefaultCase}}
4141
{{#generateOneOfAnyOfWrappers}}
42-
{{#discriminator}}
4342
import kotlinx.serialization.KSerializer
4443
import kotlinx.serialization.encoding.Decoder
4544
import kotlinx.serialization.encoding.Encoder
46-
{{/discriminator}}
4745
{{/generateOneOfAnyOfWrappers}}
4846
{{/enumUnknownDefaultCase}}
4947
{{#generateOneOfAnyOfWrappers}}
50-
{{#discriminator}}
5148
import kotlinx.serialization.SerializationException
5249
import kotlinx.serialization.descriptors.SerialDescriptor
5350
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
5451
import kotlinx.serialization.json.JsonDecoder
5552
import kotlinx.serialization.json.JsonEncoder
56-
import kotlinx.serialization.json.JsonObject
53+
import kotlinx.serialization.json.JsonElement
54+
import kotlinx.serialization.json.JsonNull
5755
import kotlinx.serialization.json.JsonPrimitive
56+
{{#discriminator}}
57+
import kotlinx.serialization.json.JsonObject
5858
import kotlinx.serialization.json.jsonObject
5959
import kotlinx.serialization.json.jsonPrimitive
6060
{{/discriminator}}
@@ -100,13 +100,17 @@ import java.io.IOException
100100
{{#discriminator}}
101101
@Serializable(with = {{classname}}Serializer::class)
102102
{{/discriminator}}
103+
{{^discriminator}}
104+
{{#serializableModel}}@KSerializable(with = {{classname}}.{{classname}}Serializer::class){{/serializableModel}}{{^serializableModel}}@Serializable(with = {{classname}}.{{classname}}Serializer::class){{/serializableModel}}
105+
{{/discriminator}}
103106
{{/generateOneOfAnyOfWrappers}}
104107
{{/kotlinx_serialization}}
105108
{{#isDeprecated}}
106109
@Deprecated(message = "This schema is deprecated.")
107110
{{/isDeprecated}}
108111
{{>additionalModelTypeAnnotations}}
109112
{{#kotlinx_serialization}}
113+
{{#discriminator}}
110114
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}sealed interface {{classname}} {
111115
{{#discriminator.mappedModels}}
112116
@JvmInline
@@ -150,6 +154,78 @@ import java.io.IOException
150154
}
151155
}
152156
}
157+
{{/discriminator}}
158+
{{^discriminator}}
159+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}data class {{classname}}(var actualInstance: Any? = null) {
160+
161+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}object {{classname}}Serializer : KSerializer<{{classname}}> {
162+
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("{{classname}}") {
163+
element("type", JsonPrimitive.serializer().descriptor)
164+
element("actualInstance", JsonElement.serializer().descriptor)
165+
}
166+
167+
override fun serialize(encoder: Encoder, value: {{classname}}) {
168+
val jsonEncoder = encoder as? JsonEncoder ?: throw SerializationException("{{classname}} can only be serialized with Json")
169+
170+
when (val instance = value.actualInstance) {
171+
{{#composedSchemas}}
172+
{{#oneOf}}
173+
{{#isPrimitiveType}}
174+
{{#isString}}
175+
is kotlin.String -> jsonEncoder.encodeString(instance)
176+
{{/isString}}
177+
{{#isBoolean}}
178+
is kotlin.Boolean -> jsonEncoder.encodeBoolean(instance)
179+
{{/isBoolean}}
180+
{{#isInteger}}
181+
{{^isLong}}
182+
is kotlin.Int -> jsonEncoder.encodeInt(instance)
183+
{{/isLong}}
184+
{{/isInteger}}
185+
{{#isLong}}
186+
is kotlin.Long -> jsonEncoder.encodeLong(instance)
187+
{{/isLong}}
188+
{{#isNumber}}
189+
{{#isDouble}}
190+
is kotlin.Double -> jsonEncoder.encodeDouble(instance)
191+
{{/isDouble}}
192+
{{#isFloat}}
193+
is kotlin.Float -> jsonEncoder.encodeFloat(instance)
194+
{{/isFloat}}
195+
{{/isNumber}}
196+
{{/isPrimitiveType}}
197+
{{^isPrimitiveType}}
198+
is {{{dataType}}} -> jsonEncoder.encodeSerializableValue({{{dataType}}}.serializer(), instance)
199+
{{/isPrimitiveType}}
200+
{{/oneOf}}
201+
{{/composedSchemas}}
202+
null -> jsonEncoder.encodeJsonElement(JsonNull)
203+
else -> throw SerializationException("Unknown type in actualInstance: ${instance::class}")
204+
}
205+
}
206+
207+
override fun deserialize(decoder: Decoder): {{classname}} {
208+
val jsonDecoder = decoder as? JsonDecoder ?: throw SerializationException("{{classname}} can only be deserialized with Json")
209+
val jsonElement = jsonDecoder.decodeJsonElement()
210+
211+
val errorMessages = mutableListOf<String>()
212+
213+
{{#composedSchemas}}
214+
{{#oneOf}}
215+
try {
216+
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
217+
return {{classname}}(actualInstance = instance)
218+
} catch (e: Exception) {
219+
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
220+
}
221+
{{/oneOf}}
222+
{{/composedSchemas}}
223+
224+
throw SerializationException("Cannot deserialize {{classname}}. Tried: ${errorMessages.joinToString(", ")}")
225+
}
226+
}
227+
}
228+
{{/discriminator}}
153229
{{/kotlinx_serialization}}
154230
{{^kotlinx_serialization}}
155231
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}data class {{classname}}(var actualInstance: Any? = null) {

modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenModelTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,40 @@ public void polymorphicKotlinxSerialization() throws IOException {
614614
TestUtils.assertFileContains(birdKt, "@SerialName(value = \"BIRD\")");
615615
}
616616

617+
@Test(description = "generate oneOf wrapper with primitive types using kotlinx_serialization")
618+
public void oneOfPrimitiveKotlinxSerialization() throws IOException {
619+
File output = Files.createTempDirectory("test").toFile();
620+
output.deleteOnExit();
621+
622+
final CodegenConfigurator configurator = new CodegenConfigurator()
623+
.setGeneratorName("kotlin")
624+
.setLibrary("jvm-retrofit2")
625+
.setAdditionalProperties(new HashMap<>() {{
626+
put(CodegenConstants.SERIALIZATION_LIBRARY, "kotlinx_serialization");
627+
put("generateOneOfAnyOfWrappers", true);
628+
}})
629+
.setInputSpec("src/test/resources/3_0/issue_19942.json")
630+
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));
631+
632+
final ClientOptInput clientOptInput = configurator.toClientOptInput();
633+
DefaultGenerator generator = new DefaultGenerator();
634+
generator.opts(clientOptInput).generate();
635+
636+
final Path oneOfModelKt = Paths.get(output + "/src/main/kotlin/org/openapitools/client/models/ObjectWithComplexOneOfId.kt");
637+
// generates data class with actualInstance (not empty sealed interface)
638+
TestUtils.assertFileContains(oneOfModelKt, "data class ObjectWithComplexOneOfId");
639+
TestUtils.assertFileContains(oneOfModelKt, "var actualInstance: Any?");
640+
// has a custom KSerializer
641+
TestUtils.assertFileContains(oneOfModelKt, "object ObjectWithComplexOneOfIdSerializer : KSerializer<ObjectWithComplexOneOfId>");
642+
// serializer handles primitive types
643+
TestUtils.assertFileContains(oneOfModelKt, "is kotlin.String -> jsonEncoder.encodeString(instance)");
644+
// serializer handles deserialization via try-each
645+
TestUtils.assertFileContains(oneOfModelKt, "decodeFromJsonElement<kotlin.String>(jsonElement)");
646+
// parent model references the oneOf wrapper type
647+
final Path parentModelKt = Paths.get(output + "/src/main/kotlin/org/openapitools/client/models/ObjectWithComplexOneOf.kt");
648+
TestUtils.assertFileContains(parentModelKt, "val id: ObjectWithComplexOneOfId?");
649+
}
650+
617651
@Test(description = "generate polymorphic jackson model")
618652
public void polymorphicJacksonSerialization() throws IOException {
619653
File output = Files.createTempDirectory("test").toFile();

0 commit comments

Comments
 (0)