You're designing a generic serializer/registry API in Kotlin that interops with Java libraries. When would you accept/store a KClass<T> versus a java.lang.Class<T>, and how do you convert at the boundary?
answer
- Kotlin surface = KClass/reified; convert to .java at the Java call
- Java hands you Class -> .kotlin for isData/sealed/nullability
- Registry key = canonical java.lang.Class (stable)
- Erasure: use KType/typeOf or TypeReference for generics
- Only bridging? avoid the kotlin-reflect dependency
basics
~10 sUse KClass in your Kotlin-facing API so callers write MyType::class and you can read Kotlin info; convert to .java at the exact point you call into Java libraries. Store whichever the consumers key on.
solid answer
~40 sExpose `KClass<T>` (or a reified type parameter) on the **Kotlin-facing** surface: callers naturally pass `MyType::class`, and you retain access to Kotlin-only metadata (`isData`, `isSealed`, nullable property types, companion objects). At the **interop boundary** — the line where you call Jackson/Spring/JPA — convert with `.java`. Internally, choose your storage key by what you look up against: if you primarily match against tokens coming from Java frameworks, store `java.lang.Class` (canonical, identity-stable map keys); if you reflect with Kotlin features, store `KClass`. Often you store the `Class` (stable key) and lazily `.kotlin` when needed. For ergonomic generic APIs prefer `inline fun <reified T>` so callers omit the token entirely and you derive both `T::class` and `T::class.java` internally — but watch erasure: a bare token can't carry `List<String>`, so use `typeOf<T>()`/`TypeReference` for parameterized payloads.
code
kotlin · 11 linesclass TypeRegistry {
private val byClass = HashMap<Class<*>, Codec<*>>() // canonical key
fun <T : Any> register(type: KClass<T>, codec: Codec<T>) {
byClass[type.java] = codec // bridge at the boundary
}
inline fun <reified T : Any> register(codec: Codec<T>) = register(T::class, codec)
fun forJava(raw: Class<*>): Codec<*>? = byClass[raw]
fun describeKotlin(raw: Class<*>) = raw.kotlin.isData // reverse bridge
}go deeper
Knows to accept MyType::class and add .java when calling the Java library.
Places the .java conversion at the interop boundary and uses .kotlin when handed a raw Class.
Chooses KClass/reified on the Kotlin surface, keys registries on canonical java.lang.Class, and handles erasure with KType/TypeReference.
Designs the whole boundary: dependency cost of kotlin-reflect, identity-stable keys across classloaders, reified+typeOf ergonomics, and when generic fidelity forces KType over a bare token.
## Decision: which handle on which surface Think in layers: ``` Kotlin callers -> [your API] -> Java libraries (Jackson/Spring/JPA) KClass<T> / reified T .java conversion here ``` ### Kotlin-facing API: prefer KClass or reified - Accepting `KClass<T>` lets callers write the idiomatic `register(MyDto::class)` and gives **you** Kotlin-only reflection (`isData`, `isSealed`, `objectInstance`, `memberProperties` with nullability). - Even better for generics ergonomics: `inline fun <reified T> register()` so callers write `register<MyDto>()` and you internally derive `T::class` and `T::class.java`. ```kotlin inline fun <reified T : Any> ObjectMapper.read(json: String): T = readValue(json, T::class.java) ``` ### Interop boundary: convert with .java Do the `.java` conversion **at the call site** into the Java library, not deep inside your domain types — keeps the boundary explicit: ```kotlin fun <T : Any> deserialize(json: String, type: KClass<T>): T = mapper.readValue(json, type.java) // bridge right here ``` ### Reverse boundary: .kotlin when Java hands you a Class For scanners/post-processors receiving a raw `Class`, convert once and work in Kotlin: ```kotlin fun describe(raw: Class<*>) { val k = raw.kotlin if (k.isData) registerDataClass(k) } ``` ## What to store as the registry key - **`java.lang.Class` as key:** canonical per classloader, so identity/`==` lookups are rock-solid and align with how Java frameworks key things. Good default for a type registry. - **`KClass` as key:** fine (it has proper `equals`/`hashCode`), and natural if you predominantly use Kotlin reflection. Don't rely on `===`. A common pattern: store `Class<*>` for the stable key and call `.kotlin` lazily for the rare Kotlin-aware path. ## Generics / erasure caveat (the senior trap) A `Class<T>` or `KClass<T>` token is the **raw** type only; due to JVM erasure it cannot express `List<String>` vs `List<Int>`. For parameterized payloads: - Jackson: `TypeReference<List<MyDto>>()` or `constructParametricType`. - Kotlin reflection: `typeOf<List<MyDto>>()` returns a full `KType` capturing arguments. So design the API to take a `KType`/`TypeReference` when generic fidelity matters, and reserve plain `KClass`/`Class` for non-generic types. ## Cost & dependency boundary `::class`, `.java`, `.kotlin` are stdlib-only and cheap. Reading members/supertypes via `KClass` pulls in **kotlin-reflect** (a real artifact + startup cost). If your library only needs `.java` handoff, you can avoid the kotlin-reflect dependency entirely. ## Summary checklist - Kotlin surface -> `KClass`/reified. - Convert with `.java` exactly at the Java-library call. - Convert with `.kotlin` when Java gives you a Class and you need Kotlin info. - Key registries on `java.lang.Class` (canonical) unless Kotlin-reflection-centric. - Use `KType`/`TypeReference` for generics. - Avoid pulling in kotlin-reflect if you only bridge types.
- Why prefer reified type parameters over an explicit KClass argument?Callers write register<T>() with no token; you derive both T::class and T::class.java internally, and inlining lets you also capture typeOf<T>() to dodge erasure.
- Your API must deserialize List<UserDto>. Why is a KClass token insufficient?Erasure: KClass/Class carry only the raw type. Use typeOf<List<UserDto>>() (KType) or Jackson's TypeReference to retain the element type.
Speak Kotlin (KClass) to your callers, switch to Java (.java) only when you step through the door into the Java library room.
saying these in an interview costs you the question
- Converting to .java deep in domain code instead of at the boundary
- Keying a registry on KClass and relying on === identity
- Using a bare Class/KClass token for generic payloads
- Pulling in kotlin-reflect when only type bridging is needed
- Forcing Kotlin callers to write MyType::class.java everywhere