Kotlin Inline Reified Functions for Type‑Safe JSON Deserialization
Why Reified Types Matter
When I first moved a Java codebase to Kotlin, the biggest friction point wasn't syntax — it was the way generic type information disappears at runtime. Every time I wrote a helper like fun , the compiler reminded me that T is erased, forcing me to pass a Class or a TypeToken. That boilerplate adds noise and a surface for bugs.
The Problem: Type Erasure in Practice
Imagine a service layer that talks to three different REST endpoints, each returning a distinct data class: User, Order, and Product. The naïve approach looks like this:
// ❌ Doesn't compile — T is erased
fun deserialize(json: String): T = Json.decodeFromString(json)
To make it work you end up with overloads or a when branch that checks T::class, which defeats the purpose of a single reusable function.
The Trick: Inline Functions with Reified Type Parameters
Kotlin lets you mark a type parameter as reified only on an inline function. The compiler then inlines the call site and substitutes the concrete type, so the generic information is available at runtime. The pattern is tiny but powerful:
// ✅ Works — T is reified
inline fun deserialize(json: String): T {
// The compiler knows the exact class of T here
return Json.decodeFromString(json)
}
Because the function is inlined, the call deserialize becomes essentially Json.decodeFromString with no extra wrapper.
Real‑World Scenario: Centralised API Parsing
In a recent project we built a thin networking layer around kotlinx.serialization. All responses funneled through a single ApiClient that performed logging, error handling, and retry logic. The client exposed a generic request method:
class ApiClient(private val http: HttpClient) {
// Centralised request logic
suspend fun request(
method: HttpMethod,
url: String,
body: Any? = null
): Result = coroutineScope {
val response = http.request(method, url) {
if (body != null) setBody(Json.encodeToString(body))
}
return@coroutineScope when (response.status) {
is HttpStatusCode.Success -> Result.success(deserialize(response.bodyAsText()))
else -> Result.failure(ApiException(response.status, response.bodyAsText()))
}
}
// The reified helper lives right next to the caller
private inline fun deserialize(json: String): T =
Json.decodeFromString(json)
}
Now every feature module calls apiClient.request