How to specify generic output type to be subclass of generic type

In Kotlin scratch file:

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?

Do you mean that the response contain total and data fields and data is a list of either Cat or Dog objects?

@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:

  1. 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.
  2. 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)
}

@madmax1028 thank you for your suggestions.

Before I posted this question, I already tried inline with reified - it’s not solving the issue.

I also tried to pass type in .readValue function, as you suggested, but the problem is that clazz has to be wrapped (passed in) in RestResponse.

Something like

return mapper.readValue(connection.inputStream, RestResponse<clazz>::class.java)

but that syntax (RestResponse<clazz>::class.java) is invalid and I can’t figure out how else to pass type inside generic class type.

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

Indeed it still had for some reason tried to deserialize objects as Pet instead of Dog. Instead I tried this version and it worked:

return mapper.readValue(connection.inputStream, object : TypeReference<RestResponse<T>>() {})
1 Like

@madmax1028 you are pro, and saviour. Thank you.

object : TypeReference<RestResponse<T>>() {}

That is really nice trick.

1 Like

Hmm, interesting. It seems like a bug (?) in the Kotlin compiler. If we create T directly, everything works as expected:

fun test1() {
    foo<MutableList<String>>()
}

inline fun <reified T> foo() {
    println("typeOf: ${typeOf<T>().javaType}")
    println("anonymous class: ${object : TypeReference<T>() {}.type}")
}

Output:

typeOf: java.util.List<java.lang.String>
anonymous class: java.util.List<java.lang.String>

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.

1 Like

@broot maybe you could report it to https://youtrack.jetbrains.com/issues/KT

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.

1 Like

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)
3 Likes

It works, thank you once again and stay awesome :slight_smile: