From 4e1816f4ca0c278d02bb9c96be647b1e2cef4568 Mon Sep 17 00:00:00 2001 From: "Kent Inge F. Simonsen" <kent.simonsen@tietoevry.com> Date: Mon, 23 May 2022 12:46:24 +0200 Subject: [PATCH 1/4] Resolves #13266 Adds option in the kotlin-jackson model generator to generate deserializers. By using generated deserializers instead of reflection based deserialization, we can achieve approximatly 3x speedup --- .../languages/KotlinClientCodegen.java | 27 +++- .../kotlin-client/data_class.mustache | 13 ++ .../kotlin-client/deserialize_value.mustache | 122 ++++++++++++++++++ .../jackson_deserializer.mustache | 66 ++++++++++ 4 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 modules/openapi-generator/src/main/resources/kotlin-client/deserialize_value.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-client/jackson_deserializer.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java index 6b9dd53321d..14332729148 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java @@ -83,6 +83,8 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen { public static final String SUPPORT_ANDROID_API_LEVEL_25_AND_BELLOW = "supportAndroidApiLevel25AndBelow"; + public static final String GENERATE_CUSTOM_JSON_DESERIALIZERS = "generateCustomJSONDeserializers"; + protected static final String VENDOR_EXTENSION_BASE_NAME_LITERAL = "x-base-name-literal"; protected String dateLibrary = DateLibrary.JAVA8.value; @@ -92,6 +94,7 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen { protected boolean useRxJava2 = false; protected boolean useRxJava3 = false; protected boolean useCoroutines = false; + protected boolean generateCustomJSONDeserializers = false; // backwards compatibility for openapi configs that specify neither rx1 nor rx2 // (mustache does not allow for boolean operators so we need this extra field) protected boolean doNotUseRxAndCoroutines = true; @@ -243,6 +246,8 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen { cliOptions.add(CliOption.newBoolean(GENERATE_ROOM_MODELS, "Generate Android Room database models in addition to API models (JVM Volley library only)", false)); 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")); + + cliOptions.add(CliOption.newBoolean(GENERATE_CUSTOM_JSON_DESERIALIZERS, "Wheter to generate custom JSON deserializers for models.")); } public CodegenType getTag() { @@ -332,6 +337,14 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen { this.roomModelPackage = roomModelPackage; } + public boolean getGenerateCustomJSONDeserializers() { + return generateCustomJSONDeserializers; + } + + public void setGenerateCustomJSONDeserializers(boolean generateCustomJSONDeserializers) { + this.generateCustomJSONDeserializers = generateCustomJSONDeserializers; + } + @Override public String modelFilename(String templateName, String modelName) { String suffix = modelTemplateFiles().get(templateName); @@ -484,6 +497,18 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen { supportingFiles.add(new SupportingFile("auth/HttpBasicAuth.kt.mustache", authFolder, "HttpBasicAuth.kt")); } } + + if(additionalProperties.containsKey(GENERATE_CUSTOM_JSON_DESERIALIZERS)) { + if(getSerializationLibrary().equals(SERIALIZATION_LIBRARY_TYPE.jackson)) { + this.setGenerateCustomJSONDeserializers(Boolean.parseBoolean(additionalProperties.get(GENERATE_CUSTOM_JSON_DESERIALIZERS).toString())); + } + else { + if(Boolean.parseBoolean(additionalProperties.get(GENERATE_CUSTOM_JSON_DESERIALIZERS).toString())){ + LOGGER.warn("Generating custom JSON deserializers is only supported with Jackson serialization."); + } + setGenerateCustomJSONDeserializers(false); + } + } } private void processDateLibrary() { @@ -776,7 +801,7 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen { for (ModelMap mo : objects.getModels()) { CodegenModel cm = mo.getModel(); - if (getGenerateRoomModels()) { + if (getGenerateRoomModels() || getGenerateCustomJSONDeserializers()) { cm.vendorExtensions.put("x-has-data-class-body", true); } diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/data_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/data_class.mustache index 8d2c28acd4a..619a2faba43 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/data_class.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/data_class.mustache @@ -14,6 +14,14 @@ import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo {{/discriminator}} +{{#generateCustomJSONDeserializers}} +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonToken +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.type.* + +{{/generateCustomJSONDeserializers}} {{/jackson}} {{#kotlinx_serialization}} import {{#serializableModel}}kotlinx.serialization.Serializable as KSerializable{{/serializableModel}}{{^serializableModel}}kotlinx.serialization.Serializable{{/serializableModel}} @@ -142,6 +150,11 @@ import {{packageName}}.infrastructure.ITransformForStorage {{/isEnum}} {{/vars}} {{/hasEnums}} +{{#jackson}} +{{#generateCustomJSONDeserializers}} +{{>jackson_deserializer}} +{{/generateCustomJSONDeserializers}} +{{/jackson}} {{#vendorExtensions.x-has-data-class-body}} } {{/vendorExtensions.x-has-data-class-body}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/deserialize_value.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/deserialize_value.mustache new file mode 100644 index 00000000000..290afab5602 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-client/deserialize_value.mustache @@ -0,0 +1,122 @@ +{{#isArray}} +{{#items.isEnum}} + "{{name}}" -> { + val list : ArrayList<{{classname}}.{{items.nameInCamelCase}}> = ArrayList() + while(p.nextToken() != JsonToken.END_ARRAY) { + list.add({{classname}}.{{items.nameInCamelCase}}.valueOf(p.text)) + } + parsedValues.{{name}} = list + } +{{/items.isEnum}} +{{^items.isEnum}} +{{#items.isString}} + "{{name}}" -> { + val list : ArrayList<String> = ArrayList() + while(p.nextToken() != JsonToken.END_ARRAY) { + list.add(p.text) + } + parsedValues.{{name}} = list + } +{{/items.isString}} +{{^items.isString}} +{{#items.isModel}} + "{{name}}" -> { + val list : ArrayList<{{&items.dataType}}> = ArrayList() + while(p.nextToken() != JsonToken.END_ARRAY) { + list.add({{&items.dataType}}.deserializer.deserialize(p, ctx)) + } + parsedValues.{{name}} = list + } +{{/items.isModel}} +{{^items.isModel}} + "{{name}}" -> parsedValues.{{name}} = ctx.readValue( + p, + {{#items.isEnum}} + ArrayType<{{classname}}.{{items.nameInCamelCase}}>::class.java + {{/items.isEnum}} + {{^items.isEnum}} + Array<{{&items.dataType}}>::class.java + {{/items.isEnum}} + ).toList() +{{/items.isModel}} +{{/items.isString}} +{{/items.isEnum}} +{{/isArray}} +{{^isArray}} +{{#isString}}{{#isEnum}} + "{{name}}" -> parsedValues.{{name}} = {{classname}}.{{nameInCamelCase}}.valueOf(p.text) +{{/isEnum}} +{{^isEnum}} + "{{name}}" -> parsedValues.{{name}} = p.text +{{/isEnum}} +{{/isString}} +{{^isString}} +{{#isInteger}} + "{{name}}" -> parsedValues.{{name}} = p.text.toInt() +{{/isInteger}} +{{^isInteger}} +{{#isLong}} + "{{name}}" -> parsedValues.{{name}} = p.text.toLong() +{{/isLong}} +{{^isLong}} +{{#isBoolean}} + "{{name}}" -> parsedValues.{{name}} = p.text.toBoolean() +{{/isBoolean}} +{{^isBoolean}} +{{#isDouble}} + "{{name}}" -> parsedValues.{{name}} = p.text.toDouble() +{{/isDouble}} +{{^isDouble}} +{{#isFloat}} + "{{name}}" -> parsedValues.{{name}} = p.text.toFloat() +{{/isFloat}} +{{^isFloat}} +{{#isDateTime}} + "{{name}}" -> parsedValues.{{name}} = java.time.OffsetDateTime.parse(p.text) +{{/isDateTime}} +{{^isDateTime}} +{{#isDate}} + "{{name}}" -> parsedValues.{{name}} = java.time.LocalDate.parse(p.text) +{{/isDate}} +{{^isDate}} +{{#isModel}} + "{{name}}" -> parsedValues.{{name}} = {{&dataType}}.deserializer.deserialize(p, ctx) +{{/isModel}} +{{^isModel}} +{{#isMap}} +{{#items.isString}} + "{{name}}" -> { + val map : HashMap<String, String> = HashMap() + while(p.nextToken() != JsonToken.END_OBJECT) { + val key = p.text + p.nextToken() + map.put(key, p.text) + } + parsedValues.{{name}} = map + } +{{/items.isString}} +{{^items.isString}} +/** {{.}} */ + "{{name}}" -> parsedValues.{{name}} = ctx.readValue( + p, + ctx.typeFactory.constructMapLikeType(HashMap::class.java, SimpleType.constructUnsafe(String::class.java), SimpleType.constructUnsafe({{items.dataType}}::class.java)) + ) +{{/items.isString}} +{{/isMap}} +{{^isMap}} +/** {{.}} */ + "{{name}}" -> parsedValues.{{name}} = ctx.readValue( + p, + {{#isEnum}}{{classname}}.{{nameInCamelCase}}::class.java{{/isEnum}}{{^isEnum}}com.fasterxml.jackson.databind.type.SimpleType.constructUnsafe({{&dataType}}::class.java){{/isEnum}} + ) +{{/isMap}} +{{/isModel}} +{{/isDate}} +{{/isDateTime}} +{{/isFloat}} +{{/isDouble}} +{{/isBoolean}} +{{/isLong}} +{{/isInteger}} +{{/isString}} +{{/isArray}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/jackson_deserializer.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/jackson_deserializer.mustache new file mode 100644 index 00000000000..82b6e1c94fb --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-client/jackson_deserializer.mustache @@ -0,0 +1,66 @@ + class ParsedValues{ + {{#requiredVars}} + {{#isArray}} + {{#items.isEnum}} + var {{name}}: List<{{classname}}.{{items.nameInCamelCase}}>? = null + {{/items.isEnum}} + {{^items.isEnum}} + var {{name}}: List<{{items.dataType}}>? = null + {{/items.isEnum}} + {{/isArray}} + {{^isArray}} + {{#isEnum}} + var {{name}}: {{classname}}.{{nameInCamelCase}}? = null + {{/isEnum}} + {{^isEnum}} + var {{name}}: {{&dataType}}? = null + {{/isEnum}} + {{/isArray}} + {{/requiredVars}} + {{#optionalVars}} + {{#isArray}} + {{#items.isEnum}} + var {{name}}: List<{{classname}}.{{items.nameInCamelCase}}>? = null + {{/items.isEnum}} + {{^items.isEnum}} + var {{name}}: List<{{items.dataType}}>? = null + {{/items.isEnum}} + {{/isArray}} + {{^isArray}} + {{#isEnum}} + var {{name}}: {{classname}}.{{nameInCamelCase}}? = null + {{/isEnum}} + {{^isEnum}} + var {{name}}: {{&dataType}}? = null + {{/isEnum}} + {{/isArray}} + {{/optionalVars}} + } + + @Suppress("UNUSED_VALUE") + class Deserializer : JsonDeserializer<{{classname}}>() { + override fun deserialize(p: JsonParser, ctx: DeserializationContext): {{classname}} { + val parsedValues = ParsedValues() + var curr = p.currentToken + if (curr != JsonToken.START_OBJECT) { + throw IllegalStateException("Should be start object") + } + curr = p.nextToken() + while (curr == JsonToken.FIELD_NAME) { + val field = p.text + curr = p.nextToken() + when (field) { +{{#requiredVars}}{{>deserialize_value}}{{/requiredVars}} +{{#optionalVars}}{{>deserialize_value}}{{/optionalVars}} + else -> p.skipChildren() + } + curr = p.nextToken() + } + return {{classname}}({{#requiredVars}} + {{name}} = parsedValues.{{name}}!!,{{/requiredVars}}{{#optionalVars}} + {{name}} = parsedValues.{{name}},{{/optionalVars}}) + } + } + companion object { + val deserializer by lazy(LazyThreadSafetyMode.NONE) { Deserializer() } + } -- GitLab From 9e70269b283db69c345f7f746a43166f340fc78f Mon Sep 17 00:00:00 2001 From: "Kent Inge F. Simonsen" <kent.simonsen@tietoevry.com> Date: Wed, 24 Aug 2022 13:11:38 +0200 Subject: [PATCH 2/4] regenerated samples and documentation --- docs/generators/kotlin.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/generators/kotlin.md b/docs/generators/kotlin.md index 7f562302f28..07236e4feeb 100644 --- a/docs/generators/kotlin.md +++ b/docs/generators/kotlin.md @@ -24,6 +24,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |collectionType|Option. Collection type to use|<dl><dt>**array**</dt><dd>kotlin.Array</dd><dt>**list**</dt><dd>kotlin.collections.List</dd></dl>|list| |dateLibrary|Option. Date library to use|<dl><dt>**threetenbp-localdatetime**</dt><dd>Threetenbp - Backport of JSR310 (jvm only, for legacy app only)</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 < 1.8)</dd></dl>|java8| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', and 'original'| |camelCase| +|generateCustomJSONDeserializers|Wheter to generate custom JSON deserializers for models.| |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 IntellJ Idea plugin and mark Kotlin main and test folders as source folders.| |false| -- GitLab From 2e931674ed2b5db0e3d6af9ce34c75ca5561f099 Mon Sep 17 00:00:00 2001 From: "Kent Inge F. Simonsen" <40064+kentis@users.noreply.github.com> Date: Fri, 2 Sep 2022 08:27:07 +0200 Subject: [PATCH 3/4] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a mispelled word Co-authored-by: Sindre Bøyum <boyum@users.noreply.github.com> --- docs/generators/kotlin.md | 2 +- .../org/openapitools/codegen/languages/KotlinClientCodegen.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/generators/kotlin.md b/docs/generators/kotlin.md index 07236e4feeb..f840ad23d0c 100644 --- a/docs/generators/kotlin.md +++ b/docs/generators/kotlin.md @@ -24,7 +24,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |collectionType|Option. Collection type to use|<dl><dt>**array**</dt><dd>kotlin.Array</dd><dt>**list**</dt><dd>kotlin.collections.List</dd></dl>|list| |dateLibrary|Option. Date library to use|<dl><dt>**threetenbp-localdatetime**</dt><dd>Threetenbp - Backport of JSR310 (jvm only, for legacy app only)</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 < 1.8)</dd></dl>|java8| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', and 'original'| |camelCase| -|generateCustomJSONDeserializers|Wheter to generate custom JSON deserializers for models.| |false| +|generateCustomJSONDeserializers|Whether to generate custom JSON deserializers for models.| |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 IntellJ Idea plugin and mark Kotlin main and test folders as source folders.| |false| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java index 14332729148..de9c4aa4b38 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java @@ -247,7 +247,7 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen { 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")); - cliOptions.add(CliOption.newBoolean(GENERATE_CUSTOM_JSON_DESERIALIZERS, "Wheter to generate custom JSON deserializers for models.")); + cliOptions.add(CliOption.newBoolean(GENERATE_CUSTOM_JSON_DESERIALIZERS, "Whether to generate custom JSON deserializers for models.")); } public CodegenType getTag() { -- GitLab From 20ba0dd8671cd6ed13dd9061362c4472dd99c83a Mon Sep 17 00:00:00 2001 From: "Kent Inge F. Simonsen" <kent.simonsen@tietoevry.com> Date: Sun, 16 Oct 2022 08:20:56 +0200 Subject: [PATCH 4/4] make clear that deserializer generation is only availbale for Jackson deserialisers in cli options --- .../org/openapitools/codegen/languages/KotlinClientCodegen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java index de9c4aa4b38..b354e25c0db 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java @@ -247,7 +247,7 @@ public class KotlinClientCodegen extends AbstractKotlinCodegen { 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")); - cliOptions.add(CliOption.newBoolean(GENERATE_CUSTOM_JSON_DESERIALIZERS, "Whether to generate custom JSON deserializers for models.")); + cliOptions.add(CliOption.newBoolean(GENERATE_CUSTOM_JSON_DESERIALIZERS, "Whether to generate custom JSON deserializers for models. (Jackson serialization only)")); } public CodegenType getTag() { -- GitLab