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
2 changes: 2 additions & 0 deletions .github/workflows/samples-kotlin-client.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ on:
- 'samples/client/petstore/kotlin*/**'
- 'samples/client/others/kotlin-jvm-okhttp-parameter-tests/**'
- samples/client/others/kotlin-integer-enum/**
- samples/client/others/kotlin-oneOf-anyOf-kotlinx-serialization/**

jobs:
build:
Expand Down Expand Up @@ -70,6 +71,7 @@ jobs:
- samples/client/others/kotlin-integer-enum
- samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization
- samples/client/others/kotlin-oneOf-discriminator-kotlinx-serialization
- samples/client/others/kotlin-oneOf-anyOf-kotlinx-serialization
steps:
- uses: actions/checkout@v5
- uses: actions/setup-java@v5
Expand Down
12 changes: 12 additions & 0 deletions bin/configs/kotlin-oneOf-anyOf-kotlinx-serialization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
generatorName: kotlin
outputDir: samples/client/others/kotlin-oneOf-anyOf-kotlinx-serialization
inputSpec: modules/openapi-generator/src/test/resources/3_0/kotlin/oneof-anyof-non-discriminator.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-client
additionalProperties:
artifactId: kotlin-oneOf-anyOf-kotlinx-serialization
serializableModel: "false"
dateLibrary: java8
library: jvm-retrofit2
enumUnknownDefaultCase: true
serializationLibrary: kotlinx_serialization
generateOneOfAnyOfWrappers: true
2 changes: 1 addition & 1 deletion docs/generators/kotlin.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', and 'original'| |original|
|explicitApi|Generates code with explicit access modifiers to comply with Kotlin Explicit API Mode.| |false|
|failOnUnknownProperties|Fail Jackson de-serialization on unknown properties| |false|
|generateOneOfAnyOfWrappers|Generate oneOf, anyOf schemas as wrappers. Only `jvm-retrofit2`(library), `gson`(serializationLibrary) support this option.| |false|
|generateOneOfAnyOfWrappers|Generate oneOf, anyOf schemas as wrappers. Only `jvm-retrofit2`(library) with `gson` or `kotlinx_serialization`(serializationLibrary) support this option.| |false|
|generateRoomModels|Generate Android Room database models in addition to API models (JVM Volley library only)| |false|
|groupId|Generated artifact package's organization (i.e. maven groupId).| |org.openapitools|
|idea|Add IntelliJ Idea plugin and mark Kotlin main and test folders as source folders.| |false|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ public KotlinClientCodegen() {

cliOptions.add(new CliOption(MAP_FILE_BINARY_TO_BYTE_ARRAY, "Map File and Binary to ByteArray (default: false)").defaultValue(Boolean.FALSE.toString()));

cliOptions.add(CliOption.newBoolean(GENERATE_ONEOF_ANYOF_WRAPPERS, "Generate oneOf, anyOf schemas as wrappers. Only `jvm-retrofit2`(library), `gson`(serializationLibrary) support this option."));
cliOptions.add(CliOption.newBoolean(GENERATE_ONEOF_ANYOF_WRAPPERS, "Generate oneOf, anyOf schemas as wrappers. Only `jvm-retrofit2`(library) with `gson` or `kotlinx_serialization`(serializationLibrary) support this option."));

CliOption serializationLibraryOpt = new CliOption(CodegenConstants.SERIALIZATION_LIBRARY, SERIALIZATION_LIBRARY_DESC);
cliOptions.add(serializationLibraryOpt.defaultValue(serializationLibrary.name()));
Expand Down Expand Up @@ -567,6 +567,7 @@ public void processOpts() {
// We replace paths like `/v1/foo/*` with `/v1/foo/<*>` to avoid this
additionalProperties.put("sanitizePathComment", new ReplaceAllLambda("\\/\\*", "/<*>"));
additionalProperties.put("fnToOneOfWrapperName", new ToOneOfWrapperName());
additionalProperties.put("fnToValueClassName", new ToValueClassName());
}

private void processDateLibrary() {
Expand Down Expand Up @@ -1155,6 +1156,28 @@ public String formatFragment(String fragment) {
}
}

private static class ToValueClassName extends CustomLambda {
@Override
public String formatFragment(String fragment) {
// Strip generic type parameters and extract simple class names
// e.g. "kotlin.collections.List<kotlin.String>" -> "ListStringValue"
// e.g. "kotlin.String" -> "StringValue"
// e.g. "User" -> "UserValue"
StringBuilder sb = new StringBuilder();
for (String part : fragment.split("[<>,]")) {
String trimmed = part.trim();
if (trimmed.isEmpty()) continue;
String simpleName = trimmed.contains(".")
? trimmed.substring(trimmed.lastIndexOf('.') + 1)
: trimmed;
sb.append(Character.toUpperCase(simpleName.charAt(0)));
sb.append(simpleName.substring(1));
}
sb.append("Value");
return sb.toString();
}
}

@Override
public void postProcess() {
System.out.println("################################################################################");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
{{/enumUnknownDefaultCase}}
{{^enumUnknownDefaultCase}}
{{#generateOneOfAnyOfWrappers}}
import kotlinx.serialization.KSerializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
{{/generateOneOfAnyOfWrappers}}
{{/enumUnknownDefaultCase}}
{{#generateOneOfAnyOfWrappers}}
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.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.encodeToJsonElement
{{/generateOneOfAnyOfWrappers}}
{{#hasEnums}}
{{/hasEnums}}
{{/kotlinx_serialization}}
Expand All @@ -57,7 +76,9 @@ import java.io.Serializable
import {{roomModelPackage}}.{{classname}}RoomModel
import {{packageName}}.infrastructure.ITransformForStorage
{{/generateRoomModels}}
{{^kotlinx_serialization}}
Copy link
Contributor

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

Choose a reason for hiding this comment

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

P2: When kotlinx_serialization is enabled and generateOneOfAnyOfWrappers is false, the template emits no class definition, leaving the generated model file empty.

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/anyof_class.mustache, line 79:

<comment>When `kotlinx_serialization` is enabled and `generateOneOfAnyOfWrappers` is false, the template emits no class definition, leaving the generated model file empty.</comment>

<file context>
@@ -57,7 +76,9 @@ import java.io.Serializable
 import {{roomModelPackage}}.{{classname}}RoomModel
 import {{packageName}}.infrastructure.ITransformForStorage
 {{/generateRoomModels}}
+{{^kotlinx_serialization}}
 import java.io.IOException
+{{/kotlinx_serialization}}
</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 is a pre-existing limitation that exists before this PR — the kotlinx_serialization + generateOneOfAnyOfWrappers=false combination for anyOf was already unsupported. This PR only adds the generateOneOfAnyOfWrappers=true path, so addressing this is out of scope here.

import java.io.IOException
{{/kotlinx_serialization}}

/**
* {{{description}}}
Expand All @@ -66,12 +87,144 @@ import java.io.IOException
{{#parcelizeModels}}
@Parcelize
{{/parcelizeModels}}
{{^generateOneOfAnyOfWrappers}}
{{#multiplatform}}{{^discriminator}}@Serializable{{/discriminator}}{{/multiplatform}}{{#kotlinx_serialization}}{{#serializableModel}}@KSerializable{{/serializableModel}}{{^serializableModel}}@Serializable{{/serializableModel}}{{/kotlinx_serialization}}{{#moshi}}{{#moshiCodeGen}}@JsonClass(generateAdapter = true){{/moshiCodeGen}}{{/moshi}}{{#jackson}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{/jackson}}
{{/generateOneOfAnyOfWrappers}}
{{#kotlinx_serialization}}
{{#generateOneOfAnyOfWrappers}}
{{#serializableModel}}@KSerializable(with = {{classname}}Serializer::class){{/serializableModel}}{{^serializableModel}}@Serializable(with = {{classname}}Serializer::class){{/serializableModel}}
{{/generateOneOfAnyOfWrappers}}
{{/kotlinx_serialization}}
{{#isDeprecated}}
@Deprecated(message = "This schema is deprecated.")
{{/isDeprecated}}
{{>additionalModelTypeAnnotations}}

{{#kotlinx_serialization}}
{{#generateOneOfAnyOfWrappers}}
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}sealed interface {{classname}} {
{{#composedSchemas}}
{{#anyOf}}
{{^vendorExtensions.x-duplicated-data-type}}
@JvmInline
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}value class {{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(val value: {{{dataType}}}) : {{classname}}

{{/vendorExtensions.x-duplicated-data-type}}
{{/anyOf}}
{{/composedSchemas}}
}

{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}object {{classname}}Serializer : KSerializer<{{classname}}> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("{{classname}}")

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

when (value) {
{{#composedSchemas}}
{{#anyOf}}
{{^vendorExtensions.x-duplicated-data-type}}
{{#isArray}}
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeJsonElement(jsonEncoder.json.encodeToJsonElement(value.value))
{{/isArray}}
{{^isArray}}
{{#isPrimitiveType}}
{{#isString}}
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeString(value.value)
{{/isString}}
{{#isBoolean}}
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeBoolean(value.value)
{{/isBoolean}}
{{#isInteger}}
{{^isLong}}
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeInt(value.value)
{{/isLong}}
{{/isInteger}}
{{#isLong}}
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeLong(value.value)
{{/isLong}}
{{#isNumber}}
{{#isDouble}}
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeDouble(value.value)
{{/isDouble}}
{{#isFloat}}
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeFloat(value.value)
{{/isFloat}}
{{/isNumber}}
{{/isPrimitiveType}}
{{^isPrimitiveType}}
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeSerializableValue({{{dataType}}}.serializer(), value.value)
{{/isPrimitiveType}}
{{/isArray}}
{{/vendorExtensions.x-duplicated-data-type}}
{{/anyOf}}
{{/composedSchemas}}
}
}

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}}
{{#anyOf}}
{{^vendorExtensions.x-duplicated-data-type}}
{{#isArray}}
if (jsonElement is JsonArray) {
try {
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
return {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(instance)
} catch (e: Exception) {
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
}
}
{{/isArray}}
{{^isArray}}
{{#isPrimitiveType}}
{{#isString}}
if (jsonElement is JsonPrimitive && jsonElement.isString) {
try {
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
return {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(instance)
} catch (e: Exception) {
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
}
}
{{/isString}}
{{^isString}}
if (jsonElement is JsonPrimitive && !jsonElement.isString) {
try {
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
return {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(instance)
} catch (e: Exception) {
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
}
}
{{/isString}}
{{/isPrimitiveType}}
{{^isPrimitiveType}}
if (jsonElement !is JsonPrimitive) {
try {
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
return {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(instance)
} catch (e: Exception) {
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
}
}
{{/isPrimitiveType}}
{{/isArray}}
{{/vendorExtensions.x-duplicated-data-type}}
{{/anyOf}}
{{/composedSchemas}}

throw SerializationException("Cannot deserialize {{classname}}. Tried: ${errorMessages.joinToString(", ")}")
}
}
{{/generateOneOfAnyOfWrappers}}
{{/kotlinx_serialization}}
{{^kotlinx_serialization}}
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}data class {{classname}}(var actualInstance: Any? = null) {

class CustomTypeAdapterFactory : TypeAdapterFactory {
Expand Down Expand Up @@ -334,4 +487,5 @@ import java.io.IOException
}
}
}
}
}
{{/kotlinx_serialization}}
Loading
Loading