You're designing a generic deserialization API that must support nested generics like List<Map<String, User>> and stay performant. Compare the runtime type-information options (KClass token, KType via typeOf, super type token / TypeReference, reified wrapper) and how you'd combine them without forcing reflection costs on hot paths.
answer
- KClass=raw, KType/TypeReference=nested
- typeOf is compile-time cheap; resolution is the cost
- Reified wrapper over non-inline core taking the descriptor
- Cache resolved ObjectReader/serializer by type
- java.lang.reflect.Type for Jackson/Spring interop
basics
~20 sA plain class token can't hold nested generics, so you need a full type model: KType (from typeOf) or a TypeReference. Offer a friendly reified entry point that builds it once, then cache the heavy reflection result so repeated calls are cheap.
solid answer
~50 sThe decision is driven by how much type structure you must preserve. A `KClass<T>`/`Class<T>` token is cheap but only holds the raw class — useless for `List<Map<String, User>>`. To keep nested arguments you need a full type model: Kotlin's `KType` from `typeOf<T>()`, or a JVM super type token (`TypeReference`/`ParameterizedTypeReference`/Gson `TypeToken`) for interop with Jackson/Spring. For ergonomics, the non-inline core should accept the heavy descriptor (`KType` or `JavaType`), and a thin `inline reified` wrapper captures it: `inline fun <reified T> read(b): T = read(b, typeOf<T>())`. For performance, the costly steps are (a) building the descriptor and (b) resolving it into a serializer/`ObjectReader`. typeOf itself is a compile-time intrinsic (cheap), but reflective resolution isn't — so cache the resolved reader keyed by `KType`/`JavaType` (e.g. `mapper.readerFor(javaType)` stored in a `ConcurrentHashMap`). That keeps reflection off the hot path while still supporting arbitrarily nested generics.
code
kotlin · 20 linesimport com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.ObjectReader
import java.util.concurrent.ConcurrentHashMap
class Json(private val mapper: ObjectMapper) {
private val readers = ConcurrentHashMap<JavaType, ObjectReader>()
fun <T> read(bytes: ByteArray, type: JavaType): T {
val reader = readers.computeIfAbsent(type) { mapper.readerFor(it) }
@Suppress("UNCHECKED_CAST")
return reader.readValue<Any>(bytes) as T
}
inline fun <reified T> read(bytes: ByteArray): T {
val t = mapper.typeFactory.constructType(object : TypeReference<T>() {})
return read(bytes, t)
}
}
// json.read<List<Map<String, User>>>(bytes) // nested generics preserved + cachedgo deeper
Can state that nested generics need more than a class token but not weigh the options.
Picks KType or TypeReference correctly and adds a reified wrapper, with basic awareness of cost.
Designs the layered core+reified API and caches resolved readers by type for hot paths.
Balances descriptor model vs interop vs reflection cost, cache bounding/leaks, and kotlin-reflect avoidance across modules.
## What each option actually preserves - **`KClass<T>` / `Class<T>` token** — raw head class only. `Map::class` loses key/value types. Cheapest, fine only for non-parameterized payloads. - **`KType` (from `kotlin.reflect.typeOf<T>()`)** — the full Kotlin type tree: `classifier` + recursive `arguments` (`KTypeProjection` with variance/nullability). Captures `List<Map<String, User>>` exactly. `typeOf` is an **inline intrinsic** baked at compile time, so producing the KType is cheap; *using* it via reflection is where cost lives. - **Super type token (`TypeReference`/`ParameterizedTypeReference`/Gson `TypeToken`)** — an anonymous subclass whose generic superclass survives erasure; yields a `java.lang.reflect.Type`. This is the interop currency for Jackson/Spring/Gson because they consume `java.lang.reflect.Type`/`JavaType`. - **`reified` wrapper** — not a type representation but the ergonomic capture mechanism: it turns the call-site type into one of the above without the caller writing it. ## Layered API shape ```kotlin class Json(private val mapper: ObjectMapper) { private val readers = java.util.concurrent.ConcurrentHashMap<JavaType, ObjectReader>() // Non-inline core: takes the heavy descriptor, caches resolution fun <T> read(bytes: ByteArray, type: JavaType): T { val reader = readers.computeIfAbsent(type) { mapper.readerFor(it) } @Suppress("UNCHECKED_CAST") return reader.readValue<Any>(bytes) as T } // Reified convenience: capture nested generics at the call site inline fun <reified T> read(bytes: ByteArray): T { val ref = object : TypeReference<T>() {} return read(bytes, mapper.typeFactory.constructType(ref)) } } ``` Caller writes `json.read<List<Map<String, User>>>(bytes)`; nested generics are preserved by the `TypeReference<T>` anonymous subclass, and the resolved `ObjectReader` is cached. ## Performance reasoning - **Descriptor construction** (`typeOf`, building a `TypeReference`) is comparatively cheap; the anonymous-subclass allocation per call is the main micro-cost — hoist/cache it if a call site is truly hot. - **Reflective resolution** (mapper resolving serializers, scanning properties) is the expensive part. Cache the resolved `ObjectReader`/serializer keyed by the type so hot paths hit a map lookup, not reflection. - **Avoid kotlin-reflect on hot paths** if you can — full `kotlin-reflect` is heavier than `java.lang.reflect`; `typeOf` doesn't require it for construction, but consuming a `KType` deeply may. ## Pitfalls - Don't expose `KClass<T>` for an API that must handle parameterized payloads — it silently drops arguments. - An `inline reified` wrapper that builds a `TypeReference` per call allocates each time; cache by call site when needed. - Mixing `KType` and `java.lang.reflect.Type` worlds requires conversion; pick the descriptor matching your serializer to avoid bridging. - Unbounded type caches can leak for dynamically generated types — bound the cache if types aren't fixed.
- Where does typeOf<T>() sit cost-wise versus full kotlin-reflect?typeOf is an inline compiler intrinsic, so constructing the KType is cheap and doesn't pull in heavy reflection; the expense appears only when you deeply traverse/resolve the KType, which may engage kotlin-reflect machinery heavier than java.lang.reflect.
- Your reified wrapper builds a new TypeReference each call. When is that a problem and how do you fix it?On a hot call site each invocation allocates an anonymous subclass; hoist the TypeReference/JavaType to a constant or cache it by type so only the first call pays the construction, then it's a map lookup.
The type descriptor is a recipe; resolving it into a reader is cooking the dish. Write the recipe cheaply via reified capture, then keep the cooked dish in a warming tray (cache) so repeat orders don't re-cook.
saying these in an interview costs you the question
- Recommending KClass<T> for nested-generic payloads
- Claiming typeOf or reflection is free on hot paths
- No caching strategy for resolved serializers/readers
- Ignoring kotlin-reflect vs java.lang.reflect cost/interop differences
- Unbounded type cache with no leak consideration