Retrieving reified generic arguments

Kotlin has a great feature that will allow you to preserve generic type T for further work, not only type resolution. So how much we can utilize it?

inline fun <reified T> isTypeOpen(): Boolean =
    T::class.isOpen

println(isTypeOpen<Int>()) // false

open class Foo
println(isTypeOpen<Foo>()) // true

This simple code does not take any “normal” argument, but it takes one generic argument – the T. Usually, we can just pass the T around to resolve other generic functions. But when the function is inlined and the generic parameter is reified, you can work with it as an actual type.

Moshi, a JSON parser, provides JsonAdapters for arbitrary types. adapter is constructed by passing a class (Java) type. To do so, we utilize the reified generic type:

inline fun <reified T> moshiAdapter(): JsonAdapter<T> {
    val moshi = Moshi.Builder().build()
    return moshi.adapter(T::class.java)
}

It seems, that this could work quite nicely, but it does not. Java class type has “erased” the generic arguments, so Moshi can’t know if you would like to get a List of ints or strings.

moshiAdapter<Int>().fromJson("1") // == 1
moshiAdapter<List<Int>>().fromJson("[1]") // fails now

Moshi’s adapter function takes Java’s Class or Java’s Type instance. So, a class has simply erased the type, we have to utilize the Type interface and its actual generic instance – ParametrizedType. Moshi provides a builder to construct Java’s ParametrizedType.

// type representing List<Int>
val type = Types.newParametrizedType(List::class.java, Int::class.java)
return moshi.adapter(type)

So how can we transform our generic type T to that Type interface? Could we somehow extract the generic parameters from the T? Yes, we can!

import kotlin.reflect.jvm.javaType

inline fun <reified T> moshiAdapter(): JsonAdapter<T> {
    val moshi = Moshi.Builder().build()
    val types = T::class.typeParameters.map { typeParameter ->
        typeParameter.upperBounds.first().javaType
    }.toTypedArray()
    val type = Types.newParameterizedType(T::class.java, *types)
    return moshi.adapter(type)
}

The code utilizes KClass and reads its type parameters, takes their actual type and constructs the ParametrizedType. This works quite nicely, but it requires kotlin-reflect. A huge lib we don’t want to distribute in Android apps. So this is functional, but we are not there yet.

Ideally, we need to read these type parameters through Java API to avoid kotlin-reflect. Obviously, reified T is a Kotlin feature and reading it by Java via ::class.java discards the type parameters. This is an impossible task.

But finally, I’ve found a great hack in StackOverflow answer by tynn. Let’s create an anonymous object that inherits from the generic type. Then we can simply read the super-type by Java reflection and obtain the Type (and the actual ParametrizedType in generic use-case).

abstract class TypeToken<T> // our own TypeToken to extend it

inline fun <reified T> moshiAdapter(): JsonAdapter<T> {
    val moshi = Moshi.Builder().build()
    val type = 
        object : TypeToken<T>() {}       // anononymous object extending our TypeToken
                                         // that is wrapping the T
        ::class.java                     // read it via Java API
        .genericSuperclass               // get type of the parent which is TypeToken<T>
        .let { it as ParameterizedType } // cast TypeToken<T> Type to ParametrizetType 
                                         // we can be sure because it is always TypeToken<T>
        .actualTypeArguments             // read the TypeToken generic args
        .first()                         // take the first (and only) generic arg - T
    return moshi.adapter(type)
}

Conclusion

In the end, the resulting code is quite easy and not so “hacky”. Please, be aware that you should use JsonClass(generatedAdapter = true)and so the code may fail in the runtime. Sadly, there is no simple way to prevent using class type without an adapter in the compile time.


Update 2020-12-15

I’ve discovered two major things which bring other solutions:

  • There is an experimental .javaType property that does not require a reflection library, it is directly in stdlib.
  • There is an experimental typeOf<T>() function that returns Kotlin’s generic type. Then you can retrieve the generics similarly and construct the adapter this way:
import kotlin.reflect.javaType

inline fun <reified T> moshiAdapter(): JsonAdapter<T> {
    val moshi = Moshi.Builder().build()
​
    val mainType = typeOf<T>()
    val finalType = Types.newParameterizedType(
        T::class.java,
        *mainType.arguments.map { it.type!!.javaType }.toTypedArray()
    )
​
    return moshi.adapter(finalType)
}