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 &lt; 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 &lt; 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