import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import java.io.InputStream
import java.net.HttpURLConnection
import java.net.URL
interface Pet
class Dog(val name: String): Pet
class Cat(val name: String): Pet
class RestResponse<T>(
val total: Int,
val data: List<T>,
)
val mapper: ObjectMapper = jacksonObjectMapper().registerKotlinModule()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
fun <T: Pet> query(clazz: Class<T>): RestResponse<T> {
val connection: HttpURLConnection = URL("https://reqres.in/api/${clazz.kotlin.simpleName}").openConnection() as HttpURLConnection
val responseStream: InputStream = connection.inputStream
return mapper.readValue(responseStream)
}
println(query(Dog::class.java))
In this case both Dog and Cat have the same fields, but pretend there are more class specific fields and in the rest api response those fields are assigned to the given class (dog or cat).
How to make the fun query to return RestResponse(Dog) when Dog class is passed as generic?
@broot that’s what I would like it to be but it’s not. This code fails because it tries to map data to Pet (which has no constructor) instead of Cat or Dog.
There are some things from me regarding this example:
mapper.readValue(responseStream) is not making a good use with neither generic parameter nor class parameter you passed. You either have to use reified generic parameter to effectively use an extention function from jackson-module-kotlin or pass a class to mapper directly. Right now you are only passing Pet type to mapper implicitly, because function is not reified and also you don’t have a proper polymorphism configured so it’s oblivious that it wouldn’t work.
Are you trying to map a whole payload as Pet object or the response body is a little more complex and contains a whole array? It’s hard to say from this code.
It’s only my estimation, but you could rewrite it this way:
fun <T : Pet> query(clazz: Class<T>): T {
val connection = URL("https://reqres.in/api/${clazz.simpleName}").openConnection() as HttpURLConnection
return mapper.readValue(connection.inputStream, clazz)
}
or:
inline fun <reified T : Pet> query(): T {
val connection = URL("https://reqres.in/api/${T::class.simpleName}").openConnection() as HttpURLConnection
return mapper.readValue<T>(connection.inputStream)
}
OK, then now it makes sense. One way to go is like this:
inline fun <reified T : Pet> query(): RestResponse<T> {
val connection = URL("https://reqres.in/api/${T::class.simpleName}").openConnection() as HttpURLConnection
return mapper.readValue<RestResponse<T>>(connection.inputStream)
}
It’s also possible to do this without reified parameter, but it’s a little less elegant.
Still, the mapper is trying to create object from Pet instead of Dog that is passed in a call
query<Dog>()
Exception is
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of org.jetbrains.kotlin.idea.scratch.generated.ScratchFileRunnerGenerated$ScratchFileRunnerGenerated$Pet (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
However, if we create intermediate bar() function which creates type for foo() from another type param, it stops working properly for anonymous class:
fun test2() {
bar<String>()
}
inline fun <reified T> bar() {
foo<MutableList<T>>()
}
Output:
typeOf: java.util.List<java.lang.String>
anonymous class: java.util.List<T>
To understand the problem, we need to look into the bytecode. It seems Kotlin generated anonymous class for test1(), as expected:
public final class Test1Kt$test1$$inlined$foo$1 extends com.fasterxml.jackson.core.type.TypeReference<java.util.List<java.lang.String>>
However, it didn’t generate anonymous class for test2(). Instead, test2() uses anonymous class generated for bar() function, which of course is generic:
public final class Test1Kt$bar$$inlined$foo$1 extends com.fasterxml.jackson.core.type.TypeReference<java.util.List<T>>
typeOf() works properly in both cases, so it seems it is technically possible and reasonable to support this case. So it seems there is a bug / limited support for reified types when used with anonymous classes.
If I may have another question, could you help me to do it without inline reified?
I know @madmax1028 mentioned that with clazz it won’t be as elegant but I have this function in a class and it is using other private functions. In order for inline function to use those private functions I would have to change them to public and make them also inline. I do not want them to be exposed/accessible from the class.
Typical solution to the problem of inline functions exposing internal components is to mark these components with @PublishedApi internal. It makes the component unavailable directly for the outside code, but it can still be used inside inline functions.
But if you are in a truly generic context and you would like to reference T by Class, then you have to construct the type manually:
val type = mapper.typeFactory.constructParametricType(RestResponse::class.java, clazz)
return mapper.readValue(responseStream, type)