Given erasure removes type arguments from instances, how can a JSON library still figure out that a field is a `List<String>` versus a `List<Int>` at runtime?
answer
- Instances erased; declaration signatures retained
- Reflection reads declared property type, not the object
- ParameterizedType / KType / typeOf<>()
- Super type token: anonymous subclass + genericSuperclass
- Bare <T> parse fails; needs KType/TypeReference/reified
basics
~20 sInstances forget their type, but the declarations (fields, method signatures) keep the generic info in class metadata. Libraries read that metadata with reflection to learn the element type — they don't ask the object itself.
solid answer
~40 sErasure removes type arguments from **instances**, but **not** from **declaration signatures**. The JVM stores generic signatures of fields, methods, and supertypes in class-file metadata, readable via `java.lang.reflect.Type` / `ParameterizedType` and, in Kotlin, via `KType`/`typeOf<>()` and reflection. So a serializer inspects the *declared* type of a property (e.g. the field `tags: List<String>`) and recovers `String`, rather than interrogating the runtime object. The classic trick for passing a fully-typed token across an API boundary is the **super type token** (an anonymous subclass capturing the type via `getGenericSuperclass()`); Jackson's `TypeReference`, Gson's `TypeToken`, and Kotlin's `typeOf<>()`/`KType` all exploit retained declaration-site generics. This is why deserializing into a bare `T` parameter fails but deserializing into a `KType`/`TypeReference` works.
code
kotlin · 13 linesimport kotlin.reflect.typeOf
import kotlin.reflect.full.memberProperties
data class Payload(val tags: List<String>, val ids: List<Int>)
fun main() {
// 1) Declaration-site generics survive in reflection:
Payload::class.memberProperties.forEach { p ->
println("${p.name}: ${p.returnType} args=${p.returnType.arguments}")
}
// 2) typeOf captures full generic type at the use site:
println(typeOf<List<String>>().arguments) // [String]
}go deeper
Understands the object doesn't know its element type and a library must look elsewhere.
Knows reflection on the declared field/property recovers the element type.
Explains retained declaration signatures vs erased instances and the super type token / typeOf/KType mechanism.
Designs serialization/boundary APIs around KType/reified, reasoning about where type info must be threaded and the failure modes of bare <T>.
## Two different places generics can live Erasure is about **instances**: a `List<String>` *object* has no element-type field. But generic info also appears in **declarations** — the types of fields, the parameter/return types of methods, and the type arguments to a superclass/interface. The JVM **keeps** these in the class file's *signature* attribute precisely so reflection and compilers across compilation units can see them. So: - **Erased:** what type a given runtime object's arguments are. - **Retained:** what type a declared property/parameter/supertype says it is. ## How a serializer uses the retained info When Jackson/kotlinx.serialization/Gson serializes an object, it reflects over the **class's declared properties**. For a property `val tags: List<String>`, reflection returns a generic type (`ParameterizedType` / Kotlin `KType`) whose argument is `String`. The library reads that, not the list instance, to choose an element (de)serializer. ```kotlin import kotlin.reflect.typeOf import kotlin.reflect.KType data class Payload(val tags: List<String>, val ids: List<Int>) // typeOf captures full generic info at the use site (compiler-synthesized): val t: KType = typeOf<List<String>>() println(t.arguments) // [String] ``` ## The super type token pattern (crossing a generic boundary) Because you can't pass `List<String>.class` (there is no such class object), libraries use an **anonymous subclass** that captures the type in its *generic superclass*, which is retained: ```kotlin abstract class TypeRef<T> { val type = (javaClass.genericSuperclass) } val ref = object : TypeRef<List<String>>() {} // captures List<String> // ref.type is a ParameterizedType exposing String ``` Jackson's `TypeReference`, Gson's `TypeToken`, and Spring's `ParameterizedTypeReference` are exactly this. Kotlin's preferred modern equivalent is **`typeOf<>()`** / **`KType`** (often via an `inline fun <reified T>` wrapper). ## Why the naive `T` approach fails ```kotlin fun <T> parse(json: String): T = TODO() // can't know T's args at runtime ``` A plain `T` is erased; inside `parse`, the function has no idea what `T` was. That's why deserialization APIs demand a `KType`, `Class`, `TypeReference`, or a `reified` inline boundary — to smuggle the retained type info in explicitly. ## Key distinctions to state - Instance erasure ≠ declaration erasure; only the former is total. - Reflection reads the *declaration site*, which is why field/property element types survive. - `typeOf<>()`/`reified` are Kotlin's call-site escape hatches; super type tokens are the language-agnostic one.
- If declaration signatures are retained, why is it still called 'type erasure'?Erasure refers specifically to the *runtime representation of instances* and the bytecode of operations: a `List<String>` object and its element accesses carry no element type. Declaration metadata is a separate, reflective channel that the running code doesn't consult during ordinary execution.
- What's the Kotlin-idiomatic way to expose a `KType` from a generic helper without the caller building a token by hand?Wrap it in `inline fun <reified T> ...` and call `typeOf<T>()` inside; the compiler fills in the concrete type at each call site, so the caller just writes `decode<List<String>>(json)`.
The instance is an unlabeled box, but the warehouse blueprint (the class declaration) still records what each shelf is meant to hold — reflection reads the blueprint, not the box.
saying these in an interview costs you the question
- Claiming the list instance itself stores its element type
- Saying generics are 100% gone so serializers must be magic/agents
- Confusing super type token with reading the runtime object's class
- Asserting a bare `<T>` function can recover `T` via reflection
- Not distinguishing instance erasure from retained declaration signatures