skip to content

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?

level: seniorimportance: should knowfreq 40%

answer

  1. Instances erased; declaration signatures retained
  2. Reflection reads declared property type, not the object
  3. ParameterizedType / KType / typeOf<>()
  4. Super type token: anonymous subclass + genericSuperclass
  5. Bare <T> parse fails; needs KType/TypeReference/reified

basics

~20 s

Instances 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 s

Erasure 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 lines
kotlin
import 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

for a junior

Understands the object doesn't know its element type and a library must look elsewhere.

for a middle

Knows reflection on the declared field/property recovers the element type.

for a senior

Explains retained declaration signatures vs erased instances and the super type token / typeOf/KType mechanism.

for a principal

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

context