Skip to content

Commit a3babc8

Browse files
naokiwakatawakata_sansan
authored andcommitted
fix(kotlin): use sealed interface for non-discriminator oneOf/anyOf in kotlinx_serialization
1 parent 9432aaf commit a3babc8

6 files changed

Lines changed: 376 additions & 6 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ public void processOpts() {
567567
// We replace paths like `/v1/foo/*` with `/v1/foo/<*>` to avoid this
568568
additionalProperties.put("sanitizePathComment", new ReplaceAllLambda("\\/\\*", "/<*>"));
569569
additionalProperties.put("fnToOneOfWrapperName", new ToOneOfWrapperName());
570+
additionalProperties.put("fnToValueClassName", new ToValueClassName());
570571
}
571572

572573
private void processDateLibrary() {
@@ -1155,6 +1156,28 @@ public String formatFragment(String fragment) {
11551156
}
11561157
}
11571158

1159+
private static class ToValueClassName extends CustomLambda {
1160+
@Override
1161+
public String formatFragment(String fragment) {
1162+
// Strip generic type parameters and extract simple class names
1163+
// e.g. "kotlin.collections.List<kotlin.String>" -> "ListStringValue"
1164+
// e.g. "kotlin.String" -> "StringValue"
1165+
// e.g. "User" -> "UserValue"
1166+
StringBuilder sb = new StringBuilder();
1167+
for (String part : fragment.split("[<>,]")) {
1168+
String trimmed = part.trim();
1169+
if (trimmed.isEmpty()) continue;
1170+
String simpleName = trimmed.contains(".")
1171+
? trimmed.substring(trimmed.lastIndexOf('.') + 1)
1172+
: trimmed;
1173+
sb.append(Character.toUpperCase(simpleName.charAt(0)));
1174+
sb.append(simpleName.substring(1));
1175+
}
1176+
sb.append("Value");
1177+
return sb.toString();
1178+
}
1179+
}
1180+
11581181
@Override
11591182
public void postProcess() {
11601183
System.out.println("################################################################################");

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

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,25 @@ import kotlinx.serialization.builtins.serializer
3737
import kotlinx.serialization.encoding.Decoder
3838
import kotlinx.serialization.encoding.Encoder
3939
{{/enumUnknownDefaultCase}}
40+
{{^enumUnknownDefaultCase}}
41+
{{#generateOneOfAnyOfWrappers}}
42+
import kotlinx.serialization.KSerializer
43+
import kotlinx.serialization.encoding.Decoder
44+
import kotlinx.serialization.encoding.Encoder
45+
{{/generateOneOfAnyOfWrappers}}
46+
{{/enumUnknownDefaultCase}}
47+
{{#generateOneOfAnyOfWrappers}}
48+
import kotlinx.serialization.SerializationException
49+
import kotlinx.serialization.descriptors.SerialDescriptor
50+
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
51+
import kotlinx.serialization.json.JsonDecoder
52+
import kotlinx.serialization.json.JsonEncoder
53+
import kotlinx.serialization.json.JsonArray
54+
import kotlinx.serialization.json.JsonElement
55+
import kotlinx.serialization.json.JsonPrimitive
56+
import kotlinx.serialization.json.decodeFromJsonElement
57+
import kotlinx.serialization.json.encodeToJsonElement
58+
{{/generateOneOfAnyOfWrappers}}
4059
{{#hasEnums}}
4160
{{/hasEnums}}
4261
{{/kotlinx_serialization}}
@@ -57,7 +76,9 @@ import java.io.Serializable
5776
import {{roomModelPackage}}.{{classname}}RoomModel
5877
import {{packageName}}.infrastructure.ITransformForStorage
5978
{{/generateRoomModels}}
79+
{{^kotlinx_serialization}}
6080
import java.io.IOException
81+
{{/kotlinx_serialization}}
6182

6283
/**
6384
* {{{description}}}
@@ -66,12 +87,138 @@ import java.io.IOException
6687
{{#parcelizeModels}}
6788
@Parcelize
6889
{{/parcelizeModels}}
90+
{{^generateOneOfAnyOfWrappers}}
6991
{{#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}}
92+
{{/generateOneOfAnyOfWrappers}}
93+
{{#kotlinx_serialization}}
94+
{{#generateOneOfAnyOfWrappers}}
95+
{{#serializableModel}}@KSerializable(with = {{classname}}Serializer::class){{/serializableModel}}{{^serializableModel}}@Serializable(with = {{classname}}Serializer::class){{/serializableModel}}
96+
{{/generateOneOfAnyOfWrappers}}
97+
{{/kotlinx_serialization}}
7098
{{#isDeprecated}}
7199
@Deprecated(message = "This schema is deprecated.")
72100
{{/isDeprecated}}
73101
{{>additionalModelTypeAnnotations}}
74102

103+
{{#kotlinx_serialization}}
104+
{{#generateOneOfAnyOfWrappers}}
105+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}sealed interface {{classname}} {
106+
{{#composedSchemas}}
107+
{{#anyOf}}
108+
@JvmInline
109+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}value class {{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(val value: {{{dataType}}}) : {{classname}}
110+
111+
{{/anyOf}}
112+
{{/composedSchemas}}
113+
}
114+
115+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}object {{classname}}Serializer : KSerializer<{{classname}}> {
116+
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("{{classname}}")
117+
118+
override fun serialize(encoder: Encoder, value: {{classname}}) {
119+
val jsonEncoder = encoder as? JsonEncoder ?: throw SerializationException("{{classname}} can only be serialized with Json")
120+
121+
when (value) {
122+
{{#composedSchemas}}
123+
{{#anyOf}}
124+
{{#isArray}}
125+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeJsonElement(jsonEncoder.json.encodeToJsonElement(value.value))
126+
{{/isArray}}
127+
{{^isArray}}
128+
{{#isPrimitiveType}}
129+
{{#isString}}
130+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeString(value.value)
131+
{{/isString}}
132+
{{#isBoolean}}
133+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeBoolean(value.value)
134+
{{/isBoolean}}
135+
{{#isInteger}}
136+
{{^isLong}}
137+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeInt(value.value)
138+
{{/isLong}}
139+
{{/isInteger}}
140+
{{#isLong}}
141+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeLong(value.value)
142+
{{/isLong}}
143+
{{#isNumber}}
144+
{{#isDouble}}
145+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeDouble(value.value)
146+
{{/isDouble}}
147+
{{#isFloat}}
148+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeFloat(value.value)
149+
{{/isFloat}}
150+
{{/isNumber}}
151+
{{/isPrimitiveType}}
152+
{{^isPrimitiveType}}
153+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeSerializableValue({{{dataType}}}.serializer(), value.value)
154+
{{/isPrimitiveType}}
155+
{{/isArray}}
156+
{{/anyOf}}
157+
{{/composedSchemas}}
158+
}
159+
}
160+
161+
override fun deserialize(decoder: Decoder): {{classname}} {
162+
val jsonDecoder = decoder as? JsonDecoder ?: throw SerializationException("{{classname}} can only be deserialized with Json")
163+
val jsonElement = jsonDecoder.decodeJsonElement()
164+
165+
val errorMessages = mutableListOf<String>()
166+
167+
{{#composedSchemas}}
168+
{{#anyOf}}
169+
{{#isArray}}
170+
if (jsonElement is JsonArray) {
171+
try {
172+
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
173+
return {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(instance)
174+
} catch (e: Exception) {
175+
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
176+
}
177+
}
178+
{{/isArray}}
179+
{{^isArray}}
180+
{{#isPrimitiveType}}
181+
{{#isString}}
182+
if (jsonElement is JsonPrimitive && jsonElement.isString) {
183+
try {
184+
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
185+
return {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(instance)
186+
} catch (e: Exception) {
187+
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
188+
}
189+
}
190+
{{/isString}}
191+
{{^isString}}
192+
if (jsonElement is JsonPrimitive && !jsonElement.isString) {
193+
try {
194+
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
195+
return {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(instance)
196+
} catch (e: Exception) {
197+
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
198+
}
199+
}
200+
{{/isString}}
201+
{{/isPrimitiveType}}
202+
{{^isPrimitiveType}}
203+
if (jsonElement !is JsonPrimitive) {
204+
try {
205+
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
206+
return {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(instance)
207+
} catch (e: Exception) {
208+
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
209+
}
210+
}
211+
{{/isPrimitiveType}}
212+
{{/isArray}}
213+
{{/anyOf}}
214+
{{/composedSchemas}}
215+
216+
throw SerializationException("Cannot deserialize {{classname}}. Tried: ${errorMessages.joinToString(", ")}")
217+
}
218+
}
219+
{{/generateOneOfAnyOfWrappers}}
220+
{{/kotlinx_serialization}}
221+
{{^kotlinx_serialization}}
75222
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}data class {{classname}}(var actualInstance: Any? = null) {
76223
77224
class CustomTypeAdapterFactory : TypeAdapterFactory {
@@ -334,4 +481,5 @@ import java.io.IOException
334481
}
335482
}
336483
}
337-
}
484+
}
485+
{{/kotlinx_serialization}}

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

Lines changed: 132 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,30 @@ 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
52+
{{^discriminator}}
53+
import kotlinx.serialization.json.JsonElement
54+
{{/discriminator}}
5555
import kotlinx.serialization.json.JsonEncoder
56+
{{#discriminator}}
5657
import kotlinx.serialization.json.JsonObject
58+
{{/discriminator}}
5759
import kotlinx.serialization.json.JsonPrimitive
60+
{{^discriminator}}
61+
import kotlinx.serialization.json.JsonArray
62+
import kotlinx.serialization.json.decodeFromJsonElement
63+
import kotlinx.serialization.json.encodeToJsonElement
64+
{{/discriminator}}
65+
{{#discriminator}}
5866
import kotlinx.serialization.json.jsonObject
5967
import kotlinx.serialization.json.jsonPrimitive
6068
{{/discriminator}}
@@ -100,13 +108,17 @@ import java.io.IOException
100108
{{#discriminator}}
101109
@Serializable(with = {{classname}}Serializer::class)
102110
{{/discriminator}}
111+
{{^discriminator}}
112+
{{#serializableModel}}@KSerializable(with = {{classname}}Serializer::class){{/serializableModel}}{{^serializableModel}}@Serializable(with = {{classname}}Serializer::class){{/serializableModel}}
113+
{{/discriminator}}
103114
{{/generateOneOfAnyOfWrappers}}
104115
{{/kotlinx_serialization}}
105116
{{#isDeprecated}}
106117
@Deprecated(message = "This schema is deprecated.")
107118
{{/isDeprecated}}
108119
{{>additionalModelTypeAnnotations}}
109120
{{#kotlinx_serialization}}
121+
{{#discriminator}}
110122
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}sealed interface {{classname}} {
111123
{{#discriminator.mappedModels}}
112124
@JvmInline
@@ -150,6 +162,123 @@ import java.io.IOException
150162
}
151163
}
152164
}
165+
{{/discriminator}}
166+
{{^discriminator}}
167+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}sealed interface {{classname}} {
168+
{{#composedSchemas}}
169+
{{#oneOf}}
170+
@JvmInline
171+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}value class {{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(val value: {{{dataType}}}) : {{classname}}
172+
173+
{{/oneOf}}
174+
{{/composedSchemas}}
175+
}
176+
177+
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}object {{classname}}Serializer : KSerializer<{{classname}}> {
178+
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("{{classname}}")
179+
180+
override fun serialize(encoder: Encoder, value: {{classname}}) {
181+
val jsonEncoder = encoder as? JsonEncoder ?: throw SerializationException("{{classname}} can only be serialized with Json")
182+
183+
when (value) {
184+
{{#composedSchemas}}
185+
{{#oneOf}}
186+
{{#isArray}}
187+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeJsonElement(jsonEncoder.json.encodeToJsonElement(value.value))
188+
{{/isArray}}
189+
{{^isArray}}
190+
{{#isPrimitiveType}}
191+
{{#isString}}
192+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeString(value.value)
193+
{{/isString}}
194+
{{#isBoolean}}
195+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeBoolean(value.value)
196+
{{/isBoolean}}
197+
{{#isInteger}}
198+
{{^isLong}}
199+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeInt(value.value)
200+
{{/isLong}}
201+
{{/isInteger}}
202+
{{#isLong}}
203+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeLong(value.value)
204+
{{/isLong}}
205+
{{#isNumber}}
206+
{{#isDouble}}
207+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeDouble(value.value)
208+
{{/isDouble}}
209+
{{#isFloat}}
210+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeFloat(value.value)
211+
{{/isFloat}}
212+
{{/isNumber}}
213+
{{/isPrimitiveType}}
214+
{{^isPrimitiveType}}
215+
is {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}} -> jsonEncoder.encodeSerializableValue({{{dataType}}}.serializer(), value.value)
216+
{{/isPrimitiveType}}
217+
{{/isArray}}
218+
{{/oneOf}}
219+
{{/composedSchemas}}
220+
}
221+
}
222+
223+
override fun deserialize(decoder: Decoder): {{classname}} {
224+
val jsonDecoder = decoder as? JsonDecoder ?: throw SerializationException("{{classname}} can only be deserialized with Json")
225+
val jsonElement = jsonDecoder.decodeJsonElement()
226+
227+
val errorMessages = mutableListOf<String>()
228+
229+
{{#composedSchemas}}
230+
{{#oneOf}}
231+
{{#isArray}}
232+
if (jsonElement is JsonArray) {
233+
try {
234+
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
235+
return {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(instance)
236+
} catch (e: Exception) {
237+
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
238+
}
239+
}
240+
{{/isArray}}
241+
{{^isArray}}
242+
{{#isPrimitiveType}}
243+
{{#isString}}
244+
if (jsonElement is JsonPrimitive && jsonElement.isString) {
245+
try {
246+
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
247+
return {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(instance)
248+
} catch (e: Exception) {
249+
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
250+
}
251+
}
252+
{{/isString}}
253+
{{^isString}}
254+
if (jsonElement is JsonPrimitive && !jsonElement.isString) {
255+
try {
256+
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
257+
return {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(instance)
258+
} catch (e: Exception) {
259+
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
260+
}
261+
}
262+
{{/isString}}
263+
{{/isPrimitiveType}}
264+
{{^isPrimitiveType}}
265+
if (jsonElement !is JsonPrimitive) {
266+
try {
267+
val instance = jsonDecoder.json.decodeFromJsonElement<{{{dataType}}}>(jsonElement)
268+
return {{classname}}.{{#fnToValueClassName}}{{{dataType}}}{{/fnToValueClassName}}(instance)
269+
} catch (e: Exception) {
270+
errorMessages.add("Failed to deserialize as {{{dataType}}}: ${e.message}")
271+
}
272+
}
273+
{{/isPrimitiveType}}
274+
{{/isArray}}
275+
{{/oneOf}}
276+
{{/composedSchemas}}
277+
278+
throw SerializationException("Cannot deserialize {{classname}}. Tried: ${errorMessages.joinToString(", ")}")
279+
}
280+
}
281+
{{/discriminator}}
153282
{{/kotlinx_serialization}}
154283
{{^kotlinx_serialization}}
155284
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}data class {{classname}}(var actualInstance: Any? = null) {

0 commit comments

Comments
 (0)