Kotlin Inline Functions with Reified Type Parameters: Type‑Safe Generics in Practice
Why reified matters
Generics in Kotlin are erased at runtime, which means a function like fun cannot inspect T when the code actually runs. That limitation forces developers to pass Class or KClass explicitly, cluttering call sites and opening the door to mismatches.
Inline + reified lets the compiler substitute the concrete type at each call site, preserving type information without any reflection overhead.
Typical pattern without reified
Before discovering the inline trick, I would write a helper that required a class token:
fun fromJson(json: String, clazz: Class): T = Gson().fromJson(json, clazz)
// Call site
val user = fromJson(jsonString, User::class.java)
Every caller must remember to supply the class literal. If the generic type is nested — say List — you need a TypeToken or a custom ParameterizedType implementation, which quickly becomes boilerplate.
Using inline and reified
Mark the function inline and the type parameter reified. The compiler then inlines the bytecode at each call site, replacing T with the actual type argument.
inline fun fromJson(json: String): T = Gson().fromJson(json, T::class.java)
// Call site — no class argument needed
val user = fromJson(jsonString)
val users = fromJson>(jsonString) // works with parameterized types too
- No reflection – the generated code directly references
User::class.javaor the syntheticTypeTokenforList. - Zero‑cost abstraction – the function body is copied to the call site, so there is no method dispatch overhead.
- Type safety – the compiler guarantees that the returned value matches the requested type.
Real‑world example: a tiny JSON helper
In a recent micro‑service I built a small wrapper around Gson that the whole team uses for request/response parsing. The wrapper exposes two inline functions — one for single objects, one for collections — and hides the library entirely.
object Json {
private val gson = GsonBuilder().setLenient().create()
inline fun decode(json: String): T = gson.fromJson(json, T::class.java)
inline fun decodeList(json: String): List {
val type = object : TypeToken>() {}.type
return gson.fromJson(json, type)
}
}
// Usage in a Ktor route
get("/users/{id}") {
val user = Json.decode(call.receiveText())
call.respond(user)
}
get("/users") {
val users = Json.decodeList(call.receiveText())
call.respond(users)
}
Notice the decodeList implementation: we still need a TypeToken because List is a parameterized type. The inline function creates an anonymous subclass of TypeToken at each call site, so the concrete element type (User) is baked in. Callers stay clean — just Json.decodeList.
Gotchas and best practices
- Don't over‑inline large functions. The bytecode is duplicated at every call site; a massive function will bloat the DEX/bytecode. Keep the body tiny — delegate to a private non‑inline implementation if necessary.
- Reified works only on type parameters of inline functions. You cannot make a class property reified, nor can you use it on a regular (non‑inline) function.
- Watch out for generic variance. If you declare
inline fun, the call site must match the exact type argument; covariance/contravariance rules still apply.foo(list: List ) - Interoperability with Java. Java callers cannot invoke an inline reified function directly; they see a synthetic method with an extra
Classparameter. Provide a non‑inline overload for Java consumers if the API is public.
Adopting this pattern has cut down the amount of Class plumbing in our codebase by roughly 40 %. The team now writes Json.decode without a second thought, and the compiler guarantees that the returned object really is a User. It's a small language feature, but it pays off every day.