Skip to content

Commit 353320c

Browse files
authored
[kotlin] better oneOf, anyOf support (#18382)
* add validteJsonElement * add oneOf support * various fixes, add tests * minor fixes * minor fixes * update data class * remove comments * array support, add test * update api client constructor * add anyOf support * add new files * fix merge * update * update * update * update
1 parent 1c7e5c4 commit 353320c

File tree

648 files changed

+8996
-6249
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

648 files changed

+8996
-6249
lines changed

bin/configs/kotlin-model-prefix-type-mapping.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ additionalProperties:
1111
library: jvm-retrofit2
1212
enumPropertyNaming: UPPERCASE
1313
serializationLibrary: gson
14+
generateOneOfAnyOfWrappers: true
1415
openapiNormalizer:
1516
SIMPLIFY_ONEOF_ANYOF: false

docs/generators/kotlin.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
2525
|collectionType|Option. Collection type to use|<dl><dt>**array**</dt><dd>kotlin.Array</dd><dt>**list**</dt><dd>kotlin.collections.List</dd></dl>|list|
2626
|dateLibrary|Option. Date library to use|<dl><dt>**threetenbp-localdatetime**</dt><dd>Threetenbp - Backport of JSR310 (jvm only, for legacy app only)</dd><dt>**kotlinx-datetime**</dt><dd>kotlinx-datetime (preferred for multiplatform)</dd><dt>**string**</dt><dd>String</dd><dt>**java8-localdatetime**</dt><dd>Java 8 native JSR310 (jvm only, for legacy app only)</dd><dt>**java8**</dt><dd>Java 8 native JSR310 (jvm only, preferred for jdk 1.8+)</dd><dt>**threetenbp**</dt><dd>Threetenbp - Backport of JSR310 (jvm only, preferred for jdk &lt; 1.8)</dd></dl>|java8|
2727
|enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', and 'original'| |original|
28+
|generateOneOfAnyOfWrappers|Generate oneOf, anyOf schemas as wrappers.| |false|
2829
|generateRoomModels|Generate Android Room database models in addition to API models (JVM Volley library only)| |false|
2930
|groupId|Generated artifact package's organization (i.e. maven groupId).| |org.openapitools|
3031
|idea|Add IntellJ Idea plugin and mark Kotlin main and test folders as source folders.| |false|

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.stream.Collectors;
2727
import java.util.stream.Stream;
2828

29+
import com.samskivert.mustache.Mustache;
2930
import org.apache.commons.lang3.StringUtils;
3031
import org.openapitools.codegen.CliOption;
3132
import org.openapitools.codegen.CodegenConstants;
@@ -89,6 +90,8 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen {
8990

9091
public static final String SUPPORT_ANDROID_API_LEVEL_25_AND_BELLOW = "supportAndroidApiLevel25AndBelow";
9192

93+
public static final String GENERATE_ONEOF_ANYOF_WRAPPERS = "generateOneOfAnyOfWrappers";
94+
9295
protected static final String VENDOR_EXTENSION_BASE_NAME_LITERAL = "x-base-name-literal";
9396

9497
protected String dateLibrary = DateLibrary.JAVA8.value;
@@ -102,11 +105,13 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen {
102105
protected boolean generateRoomModels = false;
103106
protected String roomModelPackage = "";
104107
protected boolean omitGradleWrapper = false;
108+
protected boolean generateOneOfAnyOfWrappers = true;
105109

106110
protected String authFolder;
107111

108112
protected SERIALIZATION_LIBRARY_TYPE serializationLibrary = SERIALIZATION_LIBRARY_TYPE.moshi;
109113
public static final String SERIALIZATION_LIBRARY_DESC = "What serialization library to use: 'moshi' (default), or 'gson' or 'jackson' or 'kotlinx_serialization'";
114+
110115
public enum SERIALIZATION_LIBRARY_TYPE {moshi, gson, jackson, kotlinx_serialization}
111116

112117
public enum DateLibrary {
@@ -259,6 +264,8 @@ public KotlinClientCodegen() {
259264

260265
cliOptions.add(CliOption.newBoolean(SUPPORT_ANDROID_API_LEVEL_25_AND_BELLOW, "[WARNING] This flag will generate code that has a known security vulnerability. It uses `kotlin.io.createTempFile` instead of `java.nio.file.Files.createTempFile` in order to support Android API level 25 and bellow. For more info, please check the following links https://github.com/OpenAPITools/openapi-generator/security/advisories/GHSA-23x4-m842-fmwf, https://github.com/OpenAPITools/openapi-generator/pull/9284"));
261266

267+
cliOptions.add(CliOption.newBoolean(GENERATE_ONEOF_ANYOF_WRAPPERS, "Generate oneOf, anyOf schemas as wrappers."));
268+
262269
CliOption serializationLibraryOpt = new CliOption(CodegenConstants.SERIALIZATION_LIBRARY, SERIALIZATION_LIBRARY_DESC);
263270
cliOptions.add(serializationLibraryOpt.defaultValue(serializationLibrary.name()));
264271
}
@@ -283,6 +290,10 @@ public boolean getOmitGradleWrapper() {
283290
return omitGradleWrapper;
284291
}
285292

293+
public boolean getGenerateOneOfAnyOfWrappers() {
294+
return generateOneOfAnyOfWrappers;
295+
}
296+
286297
public void setGenerateRoomModels(Boolean generateRoomModels) {
287298
this.generateRoomModels = generateRoomModels;
288299
}
@@ -332,6 +343,10 @@ public void setOmitGradleWrapper(boolean omitGradleWrapper) {
332343
this.omitGradleWrapper = omitGradleWrapper;
333344
}
334345

346+
public void setGenerateOneOfAnyOfWrappers(boolean generateOneOfAnyOfWrappers) {
347+
this.generateOneOfAnyOfWrappers = generateOneOfAnyOfWrappers;
348+
}
349+
335350
public SERIALIZATION_LIBRARY_TYPE getSerializationLibrary() {
336351
return this.serializationLibrary;
337352
}
@@ -443,6 +458,10 @@ public void processOpts() {
443458
additionalProperties.put(this.serializationLibrary.name(), true);
444459
}
445460

461+
if (additionalProperties.containsKey(GENERATE_ONEOF_ANYOF_WRAPPERS)) {
462+
setGenerateOneOfAnyOfWrappers(Boolean.parseBoolean(additionalProperties.get(GENERATE_ONEOF_ANYOF_WRAPPERS).toString()));
463+
}
464+
446465
commonSupportingFiles();
447466

448467
switch (getLibrary()) {
@@ -513,6 +532,14 @@ public void processOpts() {
513532
supportingFiles.add(new SupportingFile("auth/HttpBasicAuth.kt.mustache", authFolder, "HttpBasicAuth.kt"));
514533
}
515534
}
535+
536+
additionalProperties.put("sanitizeGeneric", (Mustache.Lambda) (fragment, writer) -> {
537+
String content = fragment.execute();
538+
for (final String s : List.of("<", ">", ",", " ", ".")) {
539+
content = content.replace(s, "");
540+
}
541+
writer.write(content);
542+
});
516543
}
517544

518545
private void processDateLibrary() {
@@ -874,7 +901,7 @@ public ModelsMap postProcessModels(ModelsMap objs) {
874901

875902
for (ModelMap mo : objects.getModels()) {
876903
CodegenModel cm = mo.getModel();
877-
if (getGenerateRoomModels()) {
904+
if (getGenerateRoomModels() || getGenerateOneOfAnyOfWrappers()) {
878905
cm.vendorExtensions.put("x-has-data-class-body", true);
879906
}
880907

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ This runs all tests and packages the library.
5959

6060
All URIs are relative to *{{{basePath}}}*
6161

62-
Class | Method | HTTP request | Description
63-
------------ | ------------- | ------------- | -------------
64-
{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{{summary}}}
62+
| Class | Method | HTTP request | Description |
63+
| ------------ | ------------- | ------------- | ------------- |
64+
{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}| *{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{{summary}}} |
6565
{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}
6666
{{/generateApiDocs}}
6767

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
{{^multiplatform}}
2+
{{#gson}}
3+
{{#generateOneOfAnyOfWrappers}}
4+
import com.google.gson.Gson
5+
import com.google.gson.JsonElement
6+
import com.google.gson.TypeAdapter
7+
import com.google.gson.TypeAdapterFactory
8+
import com.google.gson.reflect.TypeToken
9+
import com.google.gson.stream.JsonReader
10+
import com.google.gson.stream.JsonWriter
11+
import com.google.gson.annotations.JsonAdapter
12+
{{/generateOneOfAnyOfWrappers}}
13+
import com.google.gson.annotations.SerializedName
14+
{{/gson}}
15+
{{#moshi}}
16+
import com.squareup.moshi.Json
17+
import com.squareup.moshi.JsonClass
18+
{{/moshi}}
19+
{{#jackson}}
20+
{{#enumUnknownDefaultCase}}
21+
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue
22+
{{/enumUnknownDefaultCase}}
23+
import com.fasterxml.jackson.annotation.JsonProperty
24+
{{#discriminator}}
25+
import com.fasterxml.jackson.annotation.JsonSubTypes
26+
import com.fasterxml.jackson.annotation.JsonTypeInfo
27+
{{/discriminator}}
28+
{{/jackson}}
29+
{{#kotlinx_serialization}}
30+
import {{#serializableModel}}kotlinx.serialization.Serializable as KSerializable{{/serializableModel}}{{^serializableModel}}kotlinx.serialization.Serializable{{/serializableModel}}
31+
import kotlinx.serialization.SerialName
32+
import kotlinx.serialization.Contextual
33+
{{#enumUnknownDefaultCase}}
34+
import kotlinx.serialization.KSerializer
35+
import kotlinx.serialization.Serializer
36+
import kotlinx.serialization.builtins.serializer
37+
import kotlinx.serialization.encoding.Decoder
38+
import kotlinx.serialization.encoding.Encoder
39+
{{/enumUnknownDefaultCase}}
40+
{{#hasEnums}}
41+
{{/hasEnums}}
42+
{{/kotlinx_serialization}}
43+
{{#parcelizeModels}}
44+
import android.os.Parcelable
45+
import kotlinx.parcelize.Parcelize
46+
{{/parcelizeModels}}
47+
{{/multiplatform}}
48+
{{#multiplatform}}
49+
import kotlinx.serialization.*
50+
import kotlinx.serialization.descriptors.*
51+
import kotlinx.serialization.encoding.*
52+
{{/multiplatform}}
53+
{{#serializableModel}}
54+
import java.io.Serializable
55+
{{/serializableModel}}
56+
{{#generateRoomModels}}
57+
import {{roomModelPackage}}.{{classname}}RoomModel
58+
import {{packageName}}.infrastructure.ITransformForStorage
59+
{{/generateRoomModels}}
60+
import java.io.IOException
61+
62+
/**
63+
* {{{description}}}
64+
*
65+
*/
66+
{{#parcelizeModels}}
67+
@Parcelize
68+
{{/parcelizeModels}}
69+
{{#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}}
70+
{{#isDeprecated}}
71+
@Deprecated(message = "This schema is deprecated.")
72+
{{/isDeprecated}}
73+
{{>additionalModelTypeAnnotations}}
74+
{{#nonPublicApi}}internal {{/nonPublicApi}}data class {{classname}}(var actualInstance: Any? = null) {
75+
76+
class CustomTypeAdapterFactory : TypeAdapterFactory {
77+
override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
78+
if (!{{classname}}::class.java.isAssignableFrom(type.rawType)) {
79+
return null // this class only serializes '{{classname}}' and its subtypes
80+
}
81+
val elementAdapter = gson.getAdapter(JsonElement::class.java)
82+
{{#composedSchemas}}
83+
{{#anyOf}}
84+
{{^isArray}}
85+
{{^vendorExtensions.x-duplicated-data-type}}
86+
val adapter{{{dataType}}} = gson.getDelegateAdapter(this, TypeToken.get({{{dataType}}}::class.java))
87+
{{/vendorExtensions.x-duplicated-data-type}}
88+
{{/isArray}}
89+
{{#isArray}}
90+
@Suppress("UNCHECKED_CAST")
91+
val adapter{{#sanitizeGeneric}}{{{dataType}}}{{/sanitizeGeneric}} = gson.getDelegateAdapter(this, TypeToken.get(object : TypeToken<{{{dataType}}}>() {}.type)) as TypeAdapter<{{{dataType}}}>
92+
{{/isArray}}
93+
{{/anyOf}}
94+
{{/composedSchemas}}
95+
96+
@Suppress("UNCHECKED_CAST")
97+
return object : TypeAdapter<{{classname}}?>() {
98+
@Throws(IOException::class)
99+
override fun write(out: JsonWriter,value: {{classname}}?) {
100+
if (value?.actualInstance == null) {
101+
elementAdapter.write(out, null)
102+
return
103+
}
104+
105+
{{#composedSchemas}}
106+
{{#anyOf}}
107+
{{^vendorExtensions.x-duplicated-data-type}}
108+
// check if the actual instance is of the type `{{{dataType}}}`
109+
if (value.actualInstance is {{#isArray}}List<*>{{/isArray}}{{^isArray}}{{{dataType}}}{{/isArray}}) {
110+
{{#isPrimitiveType}}
111+
val primitive = adapter{{#sanitizeGeneric}}{{{dataType}}}{{/sanitizeGeneric}}.toJsonTree(value.actualInstance as {{{dataType}}}?).getAsJsonPrimitive()
112+
elementAdapter.write(out, primitive)
113+
return
114+
{{/isPrimitiveType}}
115+
{{^isPrimitiveType}}
116+
{{#isArray}}
117+
List<?> list = (List<?>) value.actualInstance
118+
if (list.get(0) is {{{items.dataType}}}) {
119+
val array = adapter{{#sanitizeGeneric}}{{{dataType}}}{{/sanitizeGeneric}}.toJsonTree(value.actualInstance as {{{dataType}}}?).getAsJsonArray()
120+
elementAdapter.write(out, array)
121+
return
122+
}
123+
{{/isArray}}
124+
{{/isPrimitiveType}}
125+
{{^isArray}}
126+
{{^isPrimitiveType}}
127+
val element = adapter{{{dataType}}}.toJsonTree(value.actualInstance as {{{dataType}}}?)
128+
elementAdapter.write(out, element)
129+
return
130+
{{/isPrimitiveType}}
131+
{{/isArray}}
132+
}
133+
{{/vendorExtensions.x-duplicated-data-type}}
134+
{{/anyOf}}
135+
{{/composedSchemas}}
136+
throw IOException("Failed to serialize as the type doesn't match anyOf schemas: {{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}")
137+
}
138+
139+
@Throws(IOException::class)
140+
override fun read(jsonReader: JsonReader): {{classname}} {
141+
val jsonElement = elementAdapter.read(jsonReader)
142+
val errorMessages = ArrayList<String>()
143+
var actualAdapter: TypeAdapter<*>
144+
val ret = {{classname}}()
145+
146+
{{#composedSchemas}}
147+
{{#anyOf}}
148+
{{^vendorExtensions.x-duplicated-data-type}}
149+
{{^hasVars}}
150+
// deserialize {{{dataType}}}
151+
try {
152+
// validate the JSON object to see if any exception is thrown
153+
{{^isArray}}
154+
{{#isNumber}}
155+
require(jsonElement.getAsJsonPrimitive().isNumber()) {
156+
String.format("Expected json element to be of type Number in the JSON string but got `%s`", jsonElement.toString())
157+
}
158+
actualAdapter = adapter{{#sanitizeGeneric}}{{{dataType}}}{{/sanitizeGeneric}}
159+
ret.actualInstance = actualAdapter.fromJsonTree(jsonElement)
160+
return ret
161+
{{/isNumber}}
162+
{{^isNumber}}
163+
{{#isPrimitiveType}}
164+
require(jsonElement.getAsJsonPrimitive().is{{#isBoolean}}Boolean{{/isBoolean}}{{#isString}}String{{/isString}}{{^isString}}{{^isBoolean}}Number{{/isBoolean}}{{/isString}}()) {
165+
String.format("Expected json element to be of type {{#isBoolean}}Boolean{{/isBoolean}}{{#isString}}String{{/isString}}{{^isString}}{{^isBoolean}}Number{{/isBoolean}}{{/isString}} in the JSON string but got `%s`", jsonElement.toString())
166+
}
167+
actualAdapter = adapter{{#sanitizeGeneric}}{{{dataType}}}{{/sanitizeGeneric}}
168+
ret.actualInstance = actualAdapter.fromJsonTree(jsonElement)
169+
return ret
170+
{{/isPrimitiveType}}
171+
{{/isNumber}}
172+
{{^isNumber}}
173+
{{^isPrimitiveType}}
174+
{{{dataType}}}.validateJsonElement(jsonElement)
175+
actualAdapter = adapter{{#sanitizeGeneric}}{{{dataType}}}{{/sanitizeGeneric}}
176+
ret.actualInstance = actualAdapter.fromJsonTree(jsonElement)
177+
return ret
178+
{{/isPrimitiveType}}
179+
{{/isNumber}}
180+
{{/isArray}}
181+
{{#isArray}}
182+
require(jsonElement.isJsonArray) {
183+
String.format("Expected json element to be a array type in the JSON string but got `%s`", jsonElement.toString())
184+
}
185+
186+
// validate array items
187+
for(element in jsonElement.getAsJsonArray()) {
188+
{{#items}}
189+
{{#isNumber}}
190+
require(jsonElement.getAsJsonPrimitive().isNumber) {
191+
String.format("Expected json element to be of type Number in the JSON string but got `%s`", jsonElement.toString())
192+
}
193+
{{/isNumber}}
194+
{{^isNumber}}
195+
{{#isPrimitiveType}}
196+
require(element.getAsJsonPrimitive().is{{#isBoolean}}Boolean{{/isBoolean}}{{#isString}}String{{/isString}}{{^isString}}{{^isBoolean}}Number{{/isBoolean}}{{/isString}}) {
197+
String.format("Expected array items to be of type {{#isBoolean}}Boolean{{/isBoolean}}{{#isString}}String{{/isString}}{{^isString}}{{^isBoolean}}Number{{/isBoolean}}{{/isString}} in the JSON string but got `%s`", jsonElement.toString())
198+
}
199+
{{/isPrimitiveType}}
200+
{{/isNumber}}
201+
{{^isNumber}}
202+
{{^isPrimitiveType}}
203+
{{{dataType}}}.validateJsonElement(element)
204+
{{/isPrimitiveType}}
205+
{{/isNumber}}
206+
{{/items}}
207+
}
208+
actualAdapter = adapter{{#sanitizeGeneric}}{{{dataType}}}{{/sanitizeGeneric}}
209+
ret.actualInstance = actualAdapter.fromJsonTree(jsonElement)
210+
return ret
211+
{{/isArray}}
212+
//log.log(Level.FINER, "Input data matches schema '{{{dataType}}}'")
213+
} catch (e: Exception) {
214+
// deserialization failed, continue
215+
errorMessages.add(String.format("Deserialization for {{{dataType}}} failed with `%s`.", e.message))
216+
//log.log(Level.FINER, "Input data does not match schema '{{{dataType}}}'", e)
217+
}
218+
{{/hasVars}}
219+
{{#hasVars}}
220+
// deserialize {{{.}}}
221+
try {
222+
// validate the JSON object to see if any exception is thrown
223+
{{.}}.validateJsonElement(jsonElement)
224+
log.log(Level.FINER, "Input data matches schema '{{{.}}}'")
225+
actualAdapter = adapter{{.}}
226+
ret.actualInstance = actualAdapter.fromJsonTree(jsonElement)
227+
return ret
228+
} catch (e: Exception) {
229+
// deserialization failed, continue
230+
errorMessages.add(String.format("Deserialization for {{{.}}} failed with `%s`.", e.message))
231+
//log.log(Level.FINER, "Input data does not match schema '{{{.}}}'", e)
232+
}
233+
{{/hasVars}}
234+
{{/vendorExtensions.x-duplicated-data-type}}
235+
{{/anyOf}}
236+
{{/composedSchemas}}
237+
238+
throw IOException(String.format("Failed deserialization for {{classname}}: no schema match result. Detailed failure message for anyOf schemas: %s. JSON: %s", errorMessages, jsonElement.toString()))
239+
}
240+
}.nullSafe() as TypeAdapter<T>
241+
}
242+
}
243+
}

0 commit comments

Comments
 (0)