What reflection capabilities are available from commonMain, and how do you design shared code that needs type information without depending on JVM-only kotlin.reflect.full?
answer
- commonMain reflection = lightweight KClass only
- members/createInstance/supertypes = JVM-only kotlin.reflect.full
- qualifiedName not supported on JS/Native
- Portable: serialization plugin, reified, sealed+when, KSP
- Isolate full reflection behind expect/actual on jvmMain
basics
~20 sIn shared code you only get lightweight class tokens via ::class (a KClass), with names and type checks. Listing properties or calling members needs JVM-only reflection, so for portable code you avoid reflection and use serialization plugins or type tokens instead.
solid answer
~40 sFrom `commonMain` the **only** reflection available is the lightweight `KClass`, obtained via `Type::class` or `instance::class`. You get `simpleName`, `isInstance(x)`, and identity/equality; `qualifiedName`, `members`, `memberProperties`, `constructors`, `createInstance()`, supertype walking, and annotation introspection live in `kotlin.reflect.full` / `kotlin.reflect.jvm` and are **JVM-only**. So a portable design must avoid runtime reflection. The idiomatic replacements: (1) **kotlinx.serialization**, which uses a **compiler plugin** to generate `KSerializer`s at compile time — no reflection, fully multiplatform; (2) **reified type parameters** (`inline fun <reified T>`) to carry a type at the call site without reflection; (3) explicit **type tokens / sealed hierarchies + `when`** for dispatch; (4) for DI/registration, code generation (KSP) rather than classpath scanning. Reserve `kotlin.reflect.full` for `jvmMain` behind an `expect/actual` if a JVM-only path genuinely needs it.
code
kotlin · 11 lines// Portable, reflection-free dispatch + serialization in commonMain
import kotlinx.serialization.*
@Serializable sealed interface Shape
@Serializable data class Circle(val r: Double) : Shape
@Serializable data class Square(val s: Double) : Shape
fun area(shape: Shape): Double = when (shape) { // exhaustive, no reflection
is Circle -> kotlin.math.PI * shape.r * shape.r
is Square -> shape.s * shape.s
}go deeper
Knows ::class gives a KClass and that deep reflection isn't generally available in shared code.
Identifies that kotlin.reflect.full is JVM-only and reaches for serialization annotations instead of reflection.
Designs with reified generics, sealed hierarchies, and the serialization plugin to avoid runtime reflection.
Sets architecture: compile-time mechanisms (plugin/KSP) as default, full reflection quarantined behind expect/actual on JVM, and reasons about startup cost and portability trade-offs.
## The reflection boundary in KMP Kotlin reflection is split: - **Common (multiplatform), lightweight**: the `KClass<T>` token from `T::class` or `value::class`. Common members are essentially `simpleName`, `isInstance(value)`, and structural equality/`hashCode`. `qualifiedName` exists in the API but is **not supported on JS/Native** (returns null/throws), so don't rely on it portably. - **JVM-only, full**: `kotlin.reflect.full` (`memberProperties`, `declaredFunctions`, `supertypes`, `createInstance`, `primaryConstructor`) and `kotlin.reflect.jvm` (bridges to `java.lang.reflect`). None of this compiles in `commonMain`. ```kotlin // commonMain — OK val k = value::class println(k.simpleName) if (String::class.isInstance(value)) { /* ... */ } // commonMain — COMPILE ERROR (JVM-only) // value::class.memberProperties ``` ## Designing without runtime reflection ### 1. kotlinx.serialization (compiler plugin, not reflection) Annotate `@Serializable` and the plugin generates a `KSerializer` at compile time. This is the canonical KMP answer to "map objects to JSON without reflection": ```kotlin import kotlinx.serialization.* import kotlinx.serialization.json.Json @Serializable data class User(val id: Int, val name: String) val json = Json.encodeToString(User(1, "Ada")) // no reflection val u = Json.decodeFromString<User>(json) ``` ### 2. Reified type parameters `inline fun <reified T>` makes `T` available at runtime at the call site (the type is inlined), avoiding a reflective lookup: ```kotlin inline fun <reified T> Json.decode(s: String): T = decodeFromString(s) ``` ### 3. Sealed hierarchies + exhaustive `when` Replace reflective dispatch with a closed type set the compiler checks: ```kotlin sealed interface Event data class Click(val x: Int) : Event object Close : Event fun handle(e: Event) = when (e) { is Click -> ...; Close -> ... } ``` ### 4. Code generation over scanning For DI/plugin registries, use **KSP** (Kotlin Symbol Processing) to generate registration code at build time instead of classpath scanning, which is inherently JVM/reflection-bound. ### 5. Quarantine JVM reflection behind expect/actual If a feature truly needs full reflection on JVM only, declare an `expect` in common and put the `kotlin.reflect.full` usage in `jvmMain`'s `actual`, with sane fallbacks on other targets. ## Why this matters architecturally Libraries that assume runtime reflection (many JVM frameworks) cannot be lifted into `commonMain`. A principal-level designer chooses **compile-time** mechanisms (plugins, reified generics, sealed types, KSP) so the shared module stays portable, smaller, and faster (no reflection startup cost), and keeps any unavoidable reflection isolated on the JVM target.
- How does kotlinx.serialization avoid reflection while still mapping fields?A compiler plugin generates a KSerializer per @Serializable type at compile time, encoding field order/names, so encode/decode is plain generated code — fully multiplatform.
- You must scan annotations to build a registry. What's the multiplatform-friendly approach?Use KSP to process annotations at build time and emit a generated registry, instead of runtime classpath scanning which is JVM/reflection-bound.
- Is qualifiedName safe to use in commonMain?No — it is unsupported on JS and Native; rely on simpleName or your own stable identifiers for portable code.
A common KClass is a name tag (who you are); full reflection is an X-ray of internals — and the X-ray machine only exists in the JVM room.
saying these in an interview costs you the question
- Calls memberProperties/createInstance from commonMain expecting it to compile
- Relies on qualifiedName for cross-platform identity
- Proposes runtime classpath scanning for a multiplatform module
- Treats kotlinx.serialization as reflection-based
- Spreads full reflection through shared code instead of isolating on JVM