How does the kotlinx.serialization @Serializable plugin generate serializers at compile time, and why is that different from reflection-based serialization?
answer
- @Serializable plugin emits KSerializer at compile time
- Generated Companion.serializer() + SerialDescriptor
- No runtime reflection -> Native/JS, R8-safe, fast
- Jackson/Gson use runtime reflection instead
- @SerialName/@Transient/@Required + SerializersModule
basics
~20 sThe @Serializable compiler plugin writes a serializer for your class while it compiles, so no runtime reflection is needed. Reflection-based tools instead inspect the class at runtime, which is slower and harder to use on native or minified targets.
solid answer
~40 sAnnotating a class with `@Serializable` triggers the kotlinx.serialization **compiler plugin** to synthesize a `KSerializer<T>` and expose it through a generated `Companion.serializer()` (and a class descriptor). Everything — field order, names, defaults — is known and encoded at compile time, so encoding/decoding does no runtime type introspection. This makes it work on **Kotlin/Native** and **Kotlin/JS** where JVM reflection is unavailable, survives R8/ProGuard minification, and is faster with no `kotlin-reflect` dependency. By contrast, Jackson/Gson use **runtime reflection** to walk fields and getters, which needs metadata preserved, struggles on native/minified builds, and is slower at startup. You drive it with a `Json` instance: `Json.encodeToString(value)` / `Json.decodeFromString<T>(text)`. Customization uses `@SerialName`, `@Transient`, `@Required`, custom `KSerializer`, and the `serializersModule` for polymorphism. There is still a reflective fallback (`serializer(kType)`) when you need a serializer for a runtime KType.
code
kotlin · 19 linesimport kotlinx.serialization.*
import kotlinx.serialization.json.Json
@Serializable
sealed interface Shape
@Serializable @SerialName("circle")
data class Circle(val r: Double) : Shape
@Serializable @SerialName("rect")
data class Rect(val w: Double, val h: Double) : Shape
fun main() {
val json = Json { prettyPrint = false }
val s: Shape = Circle(2.0)
val text = json.encodeToString(s) // {"type":"circle","r":2.0}
val back = json.decodeFromString<Shape>(text)
println(back) // Circle(r=2.0)
}go deeper
Knows you add @Serializable and use Json.encodeToString/decodeFromString.
Explains generated serializer()/descriptor and basic annotations like @SerialName/@Transient.
Contrasts compile-time codegen vs runtime reflection and the multiplatform/R8/perf consequences, plus polymorphism via SerializersModule.
Weighs library choice across platforms, schema-evolution and security (untrusted input, class discriminators), and the reflective serializer(kType) escape hatch.
## Two strategies for serialization **Reflection-based** (Jackson, Gson): at runtime the library uses reflection (`java.lang.reflect` / `kotlin-reflect`) to discover fields, getters, and constructor parameters, then reads/writes them. It needs runtime metadata intact, costs startup time, and breaks or needs config under R8/ProGuard or on platforms without JVM reflection. **Compile-time code generation** (kotlinx.serialization): a **compiler plugin** analyzes `@Serializable` classes during compilation and *emits* serialization code. No runtime introspection of the type is required. ## What @Serializable generates For `@Serializable class Foo(...)`, the plugin synthesizes: - a **`KSerializer<Foo>`** implementation with `serialize`/`deserialize`, - a **`SerialDescriptor`** describing element names, indices, optionality, - a generated `Companion` with `serializer(): KSerializer<Foo>`. ```kotlin import kotlinx.serialization.* import kotlinx.serialization.json.Json @Serializable data class User( val id: Long, @SerialName("user_name") val name: String, @Transient val cached: String = "" // skipped ) val json = Json { ignoreUnknownKeys = true } val text = json.encodeToString(User(1, "Ada")) val back = json.decodeFromString<User>(text) ``` ## Why compile-time is advantageous - **Multiplatform**: works on Kotlin/Native and Kotlin/JS where JVM reflection doesn't exist. - **Minification-safe**: survives R8/ProGuard because no runtime name lookup is needed (the serializer is explicit code). - **Performance / no reflect dependency**: faster cold start, no `kotlin-reflect` jar. - **Compile-time errors**: a non-serializable property type is caught during build, not at runtime. ## Customization knobs - `@SerialName("...")` — wire name - `@Transient` — exclude a property (must have a default) - `@Required` — force presence even if it has a default - `@EncodeDefault` — control whether defaults are emitted - Custom `KSerializer<T>` via `@Serializable(with = ...)` - `SerializersModule` (in the `Json { serializersModule = ... }` builder) for **polymorphic**/`@Polymorphic` and contextual serialization ## Reflective escape hatch When the type is only known at runtime as a `KType`, `serializer(kType)` resolves a serializer reflectively — the one place the library leans on reflection.
- Why does kotlinx.serialization work on Kotlin/Native while Jackson does not?kotlinx.serialization generates serializer code at compile time and needs no runtime reflection, whereas Jackson depends on JVM reflection that doesn't exist on Native.
- How do you serialize a sealed hierarchy polymorphically?Mark the base and subtypes @Serializable (or register them in a SerializersModule); the format emits a class discriminator (default key "type") so the right subtype is decoded.
Compile-time serialization is like printing the assembly instructions into the box at the factory; reflection-based is figuring out how to assemble it by inspecting the parts each time you open it.
saying these in an interview costs you the question
- Claiming kotlinx.serialization uses runtime reflection like Gson
- Saying it needs kotlin-reflect on the classpath
- Not knowing it works on Native/JS and survives R8
- Confusing @Transient (Kotlin's) usage rules with Java's
- Thinking @Serializable is just a marker with no codegen