Repository: cjbooms/fabrikt (kotlin)

Issue:
Client generation failing when model is untyped
Client generation is using the following as input to Jackson methods:
```
Map<String, Any>::class.java
```
This is not valid when using parameterised types. It should be migrated over to use Jackson's TypeReference object:
```
typeRef: TypeReference<T>
```

Pull request:
Return types such as `Map<String, Any>?` are now supported using TypeReference

Patch:
diff --git a/build.gradle.kts b/build.gradle.kts
index 72ef19f5..2d55324c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -58,6 +58,7 @@ dependencies {
     // Below dependencies are solely present so code examples in the test resources dir compile
     testImplementation("javax.validation:validation-api:2.0.1.Final")
     testImplementation("org.springframework:spring-webmvc:5.1.9.RELEASE")
+    testImplementation("com.squareup.okhttp3:okhttp:4.9.1")
 }
 
 tasks {
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt
index 0750fc59..f5e4484c 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt
@@ -1,7 +1,7 @@
 package com.cjbooms.fabrikt.generators
 
 import com.cjbooms.fabrikt.generators.ValidationAnnotations.fieldValid
-import com.cjbooms.fabrikt.generators.model.JacksonAnnotations
+import com.cjbooms.fabrikt.generators.model.JacksonMetadata
 import com.cjbooms.fabrikt.model.KotlinTypeInfo
 import com.cjbooms.fabrikt.model.PropertyInfo
 import com.squareup.kotlinpoet.ClassName
@@ -65,7 +65,7 @@ object PropertyUtils {
                     .addParameter("name", String::class)
                     .addParameter("value", parameterizedType)
                     .addStatement("$name[name] = value")
-                    .addAnnotation(JacksonAnnotations.anySetter)
+                    .addAnnotation(JacksonMetadata.anySetter)
                     .build()
             )
         } else {
@@ -88,14 +88,14 @@ object PropertyUtils {
                             property.addModifiers(KModifier.OVERRIDE)
                             classBuilder.addSuperclassConstructorParameter(name)
                         }
-                        property.addAnnotation(JacksonAnnotations.jacksonParameterAnnotation(oasKey))
+                        property.addAnnotation(JacksonMetadata.jacksonParameterAnnotation(oasKey))
                     }
-                    property.addAnnotation(JacksonAnnotations.jacksonPropertyAnnotation(oasKey))
+                    property.addAnnotation(JacksonMetadata.jacksonPropertyAnnotation(oasKey))
                     property.addValidationAnnotations(this)
                 }
                 ClassType.VANILLA_MODEL -> {
-                    property.addAnnotation(JacksonAnnotations.jacksonParameterAnnotation(oasKey))
-                    property.addAnnotation(JacksonAnnotations.jacksonPropertyAnnotation(oasKey))
+                    property.addAnnotation(JacksonMetadata.jacksonParameterAnnotation(oasKey))
+                    property.addAnnotation(JacksonMetadata.jacksonPropertyAnnotation(oasKey))
                     property.addValidationAnnotations(this)
                 }
             }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpSimpleClientGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpSimpleClientGenerator.kt
index 88fffeef..be8c4374 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpSimpleClientGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/client/OkHttpSimpleClientGenerator.kt
@@ -5,7 +5,6 @@ import com.cjbooms.fabrikt.generators.GeneratorUtils.functionName
 import com.cjbooms.fabrikt.generators.GeneratorUtils.getBodyResponses
 import com.cjbooms.fabrikt.generators.GeneratorUtils.getHeaderParams
 import com.cjbooms.fabrikt.generators.GeneratorUtils.getPathParams
-import com.cjbooms.fabrikt.generators.GeneratorUtils.getPrimaryAcceptMediaType
 import com.cjbooms.fabrikt.generators.GeneratorUtils.getPrimaryContentMediaType
 import com.cjbooms.fabrikt.generators.GeneratorUtils.getQueryParams
 import com.cjbooms.fabrikt.generators.GeneratorUtils.primaryPropertiesConstructor
@@ -18,7 +17,6 @@ import com.cjbooms.fabrikt.generators.GeneratorUtils.toParameterSpec
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toVarName
 import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.simpleClientName
 import com.cjbooms.fabrikt.generators.client.ClientGeneratorUtils.toClientReturnType
-import com.cjbooms.fabrikt.generators.model.JacksonModelGenerator.Companion.toModelType
 import com.cjbooms.fabrikt.model.ClientType
 import com.cjbooms.fabrikt.model.Destinations
 import com.cjbooms.fabrikt.model.GeneratedFile
@@ -50,8 +48,9 @@ class OkHttpSimpleClientGenerator(
                         .builder(functionName(resource, verb))
                         .addModifiers(KModifier.PUBLIC)
                         .addKdoc(operation.toKdoc())
-                        .addAnnotation(AnnotationSpec.builder(Throws::class)
-                            .addMember("%T::class", "ApiException".toClassName(packages.client)).build()
+                        .addAnnotation(
+                            AnnotationSpec.builder(Throws::class)
+                                .addMember("%T::class", "ApiException".toClassName(packages.client)).build()
                         )
                         .addParameters(operation.requestBody.toBodyParameterSpec(packages.base))
                         .addParameters(operation.parameters.map { it.toParameterSpec(packages.base) })
@@ -61,7 +60,8 @@ class OkHttpSimpleClientGenerator(
                                 resource,
                                 verb,
                                 operation
-                            ).toStatement())
+                            ).toStatement()
+                        )
                         .returns(operation.toClientReturnType(packages))
                         .build()
                 }
@@ -147,8 +147,19 @@ data class SimpleClientOperationStatement(
     private fun CodeBlock.Builder.addQueryParamStatement(): CodeBlock.Builder {
         operation.getQueryParams().map {
             when (KotlinTypeInfo.from(it.schema)) {
-                is KotlinTypeInfo.Array -> this.add("\n.%T(%S, %N, %S)", "queryParam".toClassName(packages.client), it.name, it.name.toKCodeName(), ",")
-                else -> this.add("\n.%T(%S, %N)", "queryParam".toClassName(packages.client), it.name, it.name.toKCodeName())
+                is KotlinTypeInfo.Array -> this.add(
+                    "\n.%T(%S, %N, %S)",
+                    "queryParam".toClassName(packages.client),
+                    it.name,
+                    it.name.toKCodeName(),
+                    ","
+                )
+                else -> this.add(
+                    "\n.%T(%S, %N)",
+                    "queryParam".toClassName(packages.client),
+                    it.name,
+                    it.name.toKCodeName()
+                )
             }
         }
         return this.add("\n.build()\n")
@@ -180,12 +191,8 @@ data class SimpleClientOperationStatement(
         return this.add("\n.build()\n")
     }
 
-    private fun CodeBlock.Builder.addRequestExecutionStatement(): CodeBlock.Builder {
-        val returnType = operation.getPrimaryAcceptMediaType()?.value?.schema?.let {
-            toModelType(packages.base, KotlinTypeInfo.from(it))
-        }
-        return this.add("\nreturn request.execute(client, objectMapper, %T::class.java)", returnType ?: Unit::class.asTypeName())
-    }
+    private fun CodeBlock.Builder.addRequestExecutionStatement() =
+        this.add("\nreturn request.execute(client, objectMapper, jacksonTypeRef())")
 
     private fun CodeBlock.Builder.addRequestSerializerStatement(verb: String) {
         val requestBody = operation.requestBody
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonAnnotations.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonMetadata.kt
similarity index 94%
rename from src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonAnnotations.kt
rename to src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonMetadata.kt
index 5dd70493..ed6c6150 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonAnnotations.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonMetadata.kt
@@ -5,7 +5,8 @@ import com.squareup.kotlinpoet.ClassName
 import com.squareup.kotlinpoet.CodeBlock
 import com.squareup.kotlinpoet.TypeName
 
-object JacksonAnnotations {
+object JacksonMetadata {
+    val TYPE_REFERENCE_IMPORT = Pair("com.fasterxml.jackson.module.kotlin", "jacksonTypeRef")
     private val JSON_PROPERTY_CLASS = ClassName("com.fasterxml.jackson.annotation", "JsonProperty")
     private val JSON_VALUE_CLASS = ClassName("com.fasterxml.jackson.annotation", "JsonValue")
     private val JSON_TYPE_INFO_CLASS = ClassName("com.fasterxml.jackson.annotation", "JsonTypeInfo")
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
index 332c8be1..e96358cc 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
@@ -10,9 +10,9 @@ import com.cjbooms.fabrikt.generators.TypeFactory.createList
 import com.cjbooms.fabrikt.generators.TypeFactory.createMapOfMapsStringToStringAny
 import com.cjbooms.fabrikt.generators.TypeFactory.createMapOfStringToType
 import com.cjbooms.fabrikt.generators.TypeFactory.createMutableMapOfStringToType
-import com.cjbooms.fabrikt.generators.model.JacksonAnnotations.JSON_VALUE
-import com.cjbooms.fabrikt.generators.model.JacksonAnnotations.basePolymorphicType
-import com.cjbooms.fabrikt.generators.model.JacksonAnnotations.polymorphicSubTypes
+import com.cjbooms.fabrikt.generators.model.JacksonMetadata.JSON_VALUE
+import com.cjbooms.fabrikt.generators.model.JacksonMetadata.basePolymorphicType
+import com.cjbooms.fabrikt.generators.model.JacksonMetadata.polymorphicSubTypes
 import com.cjbooms.fabrikt.model.Destinations.modelsPackage
 import com.cjbooms.fabrikt.model.GeneratedType
 import com.cjbooms.fabrikt.model.KotlinTypeInfo
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinGenModels.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinGenModels.kt
index 440f6154..331fa4d8 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinGenModels.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinGenModels.kt
@@ -1,6 +1,7 @@
 package com.cjbooms.fabrikt.model
 
 import com.cjbooms.fabrikt.generators.controller.metadata.SpringImports
+import com.cjbooms.fabrikt.generators.model.JacksonMetadata
 import com.cjbooms.fabrikt.model.Destinations.clientPackage
 import com.cjbooms.fabrikt.model.Destinations.controllersPackage
 import com.cjbooms.fabrikt.model.Destinations.modelsPackage
@@ -46,7 +47,11 @@ data class Models(val models: Collection<ModelType>) : KotlinTypes(models) {
 }
 
 data class Clients(val clients: Collection<ClientType>) : KotlinTypes(clients) {
-    override val files: Collection<FileSpec> = clients.toFileSpec()
+    override val files: Collection<FileSpec> = super.files.map {
+        it.toBuilder()
+            .addImport(JacksonMetadata.TYPE_REFERENCE_IMPORT.first, JacksonMetadata.TYPE_REFERENCE_IMPORT.second)
+            .build()
+    }
 }
 
 data class Controllers(val controllers: Collection<ControllerType>) : KotlinTypes(controllers) {
@@ -61,8 +66,6 @@ data class Controllers(val controllers: Collection<ControllerType>) : KotlinType
     }
 }
 
-data class MethodSignature(val name: String, val returnType: TypeName, val parameters: List<IncomingParameter>)
-
 fun <T : GeneratedType> Collection<T>.toFileSpec(): Collection<FileSpec> = groupClasses(this)
     .map {
         val builder = FileSpec.builder(it.key.destinationPackage, it.key.className.simpleName)
@@ -76,14 +79,13 @@ fun <T : GeneratedType> Collection<T>.toFileSpec(): Collection<FileSpec> = group
 private fun <T : GeneratedType> groupClasses(allModels: Collection<T>): Map<T, List<T>> {
     val sealedClasses = allModels
         .filter { it.spec.modifiers.contains(KModifier.SEALED) }
-        .map { sealedClass ->
-            sealedClass to allModels.filter { maybeImpl -> maybeImpl.spec.superclass == sealedClass.className }
-        }.toMap()
+        .associateWith { sealedClass ->
+            allModels.filter { maybeImpl -> maybeImpl.spec.superclass == sealedClass.className }
+        }
     val otherClasses = allModels
         .filterNot { modelType -> sealedClasses.keys.contains(modelType) }
         .filterNot { modelType -> sealedClasses.values.flatten().contains(modelType) }
-        .map { it to emptyList<T>() }
-        .toMap()
+        .associateWith { emptyList<T>() }
     return sealedClasses.plus(otherClasses)
 }
 
@@ -120,12 +122,3 @@ class RequestParameter(
         defaultValue = parameter.schema.default
     )
 }
-
-object MethodNames {
-    const val READ = "read"
-    const val QUERY = "query"
-    const val CREATE = "create"
-    const val UPDATE = "update"
-    const val REMOVE_SUBRESOURCE = "removeSubresource"
-    const val ADD_SUBRESOURCE = "addSubresource"
-}
diff --git a/src/main/resources/templates/client-code/http-util.kt.hbs b/src/main/resources/templates/client-code/http-util.kt.hbs
index be9dfd0b..043329c2 100644
--- a/src/main/resources/templates/client-code/http-util.kt.hbs
+++ b/src/main/resources/templates/client-code/http-util.kt.hbs
@@ -1,5 +1,6 @@
 package {{ client }}
 
+import com.fasterxml.jackson.core.type.TypeReference
 import com.fasterxml.jackson.databind.ObjectMapper
 import okhttp3.FormBody
 import okhttp3.Headers
@@ -34,11 +35,11 @@ fun Headers.Builder.header(key: String, value: String?): Headers.Builder {
 }
 
 @Throws(ApiException::class)
-fun <T> Request.execute(client: OkHttpClient, objectMapper: ObjectMapper, classOfT: Class<T>): ApiResponse<T?> =
+fun <T> Request.execute(client: OkHttpClient, objectMapper: ObjectMapper, typeRef: TypeReference<T>): ApiResponse<T?> =
     client.newCall(this).execute().use { response ->
         when {
             response.isSuccessful ->
-                ApiResponse(response.code, response.headers, response.body?.deserialize(objectMapper, classOfT))
+                ApiResponse(response.code, response.headers, response.body?.deserialize(objectMapper, typeRef))
             response.isBadRequest() ->
                 throw ApiClientException(response.code, response.headers, response.errorMessage())
             response.isServerError() ->
@@ -53,8 +54,8 @@ fun String.pathParam(vararg params: Pair<String, Any>): String = params.asSequen
         this.replace(param.first, param.second.toString())
     }
 
-fun <T> ResponseBody.deserialize(objectMapper: ObjectMapper, classOfT: Class<T>) =
-    this.string().isNotBlankOrNull()?.let { objectMapper.readValue(it, classOfT) }
+fun <T> ResponseBody.deserialize(objectMapper: ObjectMapper, typeRef: TypeReference<T>): T? =
+    this.string().isNotBlankOrNull()?.let { objectMapper.readValue(it, typeRef) }
 
 fun String?.isNotBlankOrNull() = if (this.isNullOrBlank()) null else this
 
@@ -62,4 +63,4 @@ private fun Response.errorMessage(): String = this.body?.string() ?: this.messag
 
 private fun Response.isBadRequest(): Boolean = this.code in 400..499
 
-private fun Response.isServerError(): Boolean = this.code in 500..599
\ No newline at end of file
+private fun Response.isServerError(): Boolean = this.code in 500..599


Test patch:
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt
index 1af24fce..7eb0e261 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/OkHttpClientGeneratorTest.kt
@@ -5,6 +5,7 @@ import com.cjbooms.fabrikt.cli.CodeGenerationType
 import com.cjbooms.fabrikt.configurations.Packages
 import com.cjbooms.fabrikt.generators.client.OkHttpEnhancedClientGenerator
 import com.cjbooms.fabrikt.generators.client.OkHttpSimpleClientGenerator
+import com.cjbooms.fabrikt.generators.model.JacksonMetadata
 import com.cjbooms.fabrikt.generators.model.JacksonModelGenerator
 import com.cjbooms.fabrikt.model.ClientType
 import com.cjbooms.fabrikt.model.Models
@@ -130,6 +131,7 @@ class OkHttpClientGeneratorTest {
         this.forEach {
             val builder = singleFileBuilder
                 .addType(it.spec)
+                .addImport(JacksonMetadata.TYPE_REFERENCE_IMPORT.first, JacksonMetadata.TYPE_REFERENCE_IMPORT.second)
             builder.build()
         }
         return Linter.lintString(singleFileBuilder.build().toString())
diff --git a/src/test/resources/examples/okHttpClient/client/ApiClient.kt b/src/test/resources/examples/okHttpClient/client/ApiClient.kt
index 5a9cefba..76e3e7cc 100644
--- a/src/test/resources/examples/okHttpClient/client/ApiClient.kt
+++ b/src/test/resources/examples/okHttpClient/client/ApiClient.kt
@@ -1,6 +1,7 @@
 package examples.okHttpClient.client
 
 import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
 import examples.okHttpClient.models.Content
 import examples.okHttpClient.models.FirstModel
 import examples.okHttpClient.models.QueryResult
@@ -50,7 +51,7 @@ class ExamplePathClient(
             .get()
             .build()
 
-        return request.execute(client, objectMapper, QueryResult::class.java)
+        return request.execute(client, objectMapper, jacksonTypeRef())
     }
 
     /**
@@ -75,7 +76,7 @@ class ExamplePathClient(
             .post(objectMapper.writeValueAsString(generatedType).toRequestBody("application/json".toMediaType()))
             .build()
 
-        return request.execute(client, objectMapper, Unit::class.java)
+        return request.execute(client, objectMapper, jacksonTypeRef())
     }
 
     /**
@@ -109,7 +110,7 @@ class ExamplePathClient(
             .get()
             .build()
 
-        return request.execute(client, objectMapper, Content::class.java)
+        return request.execute(client, objectMapper, jacksonTypeRef())
     }
 
     /**
@@ -142,7 +143,7 @@ class ExamplePathClient(
             .head()
             .build()
 
-        return request.execute(client, objectMapper, Unit::class.java)
+        return request.execute(client, objectMapper, jacksonTypeRef())
     }
 
     /**
@@ -173,7 +174,7 @@ class ExamplePathClient(
             .put(objectMapper.writeValueAsString(firstModel).toRequestBody("application/json".toMediaType()))
             .build()
 
-        return request.execute(client, objectMapper, Unit::class.java)
+        return request.execute(client, objectMapper, jacksonTypeRef())
     }
 }
 
@@ -214,6 +215,6 @@ class ExamplePathSubresourceClient(
             .put(objectMapper.writeValueAsString(firstModel).toRequestBody("application/json".toMediaType()))
             .build()
 
-        return request.execute(client, objectMapper, Unit::class.java)
+        return request.execute(client, objectMapper, jacksonTypeRef())
     }
 }
diff --git a/src/test/resources/examples/okHttpClient/client/HttpUtil.kt b/src/test/resources/examples/okHttpClient/client/HttpUtil.kt
index fd81b385..13b61510 100644
--- a/src/test/resources/examples/okHttpClient/client/HttpUtil.kt
+++ b/src/test/resources/examples/okHttpClient/client/HttpUtil.kt
@@ -1,5 +1,6 @@
 package examples.okHttpClient.client
 
+import com.fasterxml.jackson.core.type.TypeReference
 import com.fasterxml.jackson.databind.ObjectMapper
 import okhttp3.FormBody
 import okhttp3.Headers
@@ -34,11 +35,11 @@ fun Headers.Builder.header(key: String, value: String?): Headers.Builder {
 }
 
 @Throws(ApiException::class)
-fun <T> Request.execute(client: OkHttpClient, objectMapper: ObjectMapper, classOfT: Class<T>): ApiResponse<T?> =
+fun <T> Request.execute(client: OkHttpClient, objectMapper: ObjectMapper, typeRef: TypeReference<T>): ApiResponse<T?> =
     client.newCall(this).execute().use { response ->
         when {
             response.isSuccessful ->
-                ApiResponse(response.code, response.headers, response.body?.deserialize(objectMapper, classOfT))
+                ApiResponse(response.code, response.headers, response.body?.deserialize(objectMapper, typeRef))
             response.isBadRequest() ->
                 throw ApiClientException(response.code, response.headers, response.errorMessage())
             response.isServerError() ->
@@ -53,8 +54,8 @@ fun String.pathParam(vararg params: Pair<String, Any>): String = params.asSequen
         this.replace(param.first, param.second.toString())
     }
 
-fun <T> ResponseBody.deserialize(objectMapper: ObjectMapper, classOfT: Class<T>) =
-    this.string().isNotBlankOrNull()?.let { objectMapper.readValue(it, classOfT) }
+fun <T> ResponseBody.deserialize(objectMapper: ObjectMapper, typeRef: TypeReference<T>): T? =
+    this.string().isNotBlankOrNull()?.let { objectMapper.readValue(it, typeRef) }
 
 fun String?.isNotBlankOrNull() = if (this.isNullOrBlank()) null else this
 
@@ -62,4 +63,4 @@ private fun Response.errorMessage(): String = this.body?.string() ?: this.messag
 
 private fun Response.isBadRequest(): Boolean = this.code in 400..499
 
-private fun Response.isServerError(): Boolean = this.code in 500..599
\ No newline at end of file
+private fun Response.isServerError(): Boolean = this.code in 500..599