skip to content

@Serializable & Plugin Codegen

The compiler plugin generates a serializer, a descriptor, and the encode/decode logic for each @Serializable class at compile time. No reflection means it works on Native and JS and starts instantly — the pitch against Jackson-style libraries.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

What does the @Serializable annotation actually do at compile time, and why doesn't kotlinx.serialization need runtime reflection like many Java JSON libraries?

level: juniorimportance: must knowfreq 70%

answer

  1. Compiler plugin, not runtime reflection
  2. Generates $serializer + SerialDescriptor + serialize/deserialize
  3. Encodes by index, not by reflected name
  4. Enables Native/JS + R8-safe
  5. Apply Gradle plugin kotlin.plugin.serialization

basics

~10 s

Adding @Serializable tells a compiler plugin to generate the code that reads and writes the class. Because that code is built when you compile, the library doesn't have to inspect the class at runtime.

solid answer

~30 s

@Serializable is a marker processed by the kotlinx.serialization compiler plugin. During compilation the plugin generates, for each annotated class, a nested synthetic object named $serializer (a KSerializer), a SerialDescriptor describing the fields, and serialize/deserialize functions that read/write each property by index. So the serialization logic is hard-coded at compile time rather than discovered via Java/Kotlin reflection at runtime. This makes it fast, ProGuard/R8-friendly, and Kotlin/Native and Kotlin/JS compatible, where full reflection isn't available. You retrieve the generated serializer with the synthesized companion method Foo.serializer() or the reified serializer<Foo>().

code

kotlin · 8 lines
kotlin
@Serializable
data class User(val id: Int, val name: String)

fun main() {
    val text = Json.encodeToString(User(1, "Ann")) // {"id":1,"name":"Ann"}
    val user = Json.decodeFromString<User>(text)
    println(User.serializer().descriptor.serialName) // User
}

go deeper

for a junior

Knows @Serializable lets you call Json.encodeToString and that codegen happens at compile time, not via reflection.

for a middle

Names the three generated artifacts ($serializer, SerialDescriptor, serialize/deserialize) and that encoding is index-based.

for a senior

Explains the multiplatform/R8/perf motivation and how serializer() vs serializer<T>() resolve to the generated object.

for a principal

Can contrast compiler-plugin codegen with KSP/reflection approaches and reason about build-time guarantees, shrinker rules, and IR-level synthesis tradeoffs.

## The problem reflection solves (and its cost) Many JVM JSON libraries (Jackson, Gson) inspect a class **at runtime** using **reflection** — APIs like `Class.getDeclaredFields()` — to discover property names and types, then read/write them. Reflection is flexible but slow to warm up, hard for ProGuard/R8 to shrink (fields can't be safely removed/renamed), and unavailable on **Kotlin/Native** and limited on **Kotlin/JS**. ## What @Serializable triggers kotlinx.serialization moves this work to **compile time** via a **compiler plugin** (you apply the Gradle plugin `org.jetbrains.kotlin.plugin.serialization`). When you annotate a class with `@Serializable`, the plugin synthesizes three things into the bytecode: - A nested object **`$serializer`** implementing `KSerializer<T>` — the actual read/write engine for that type. - A **`SerialDescriptor`** — metadata listing each property's serial name, index, type, and whether it's optional/nullable. Format engines (JSON, ProtoBuf) walk this to know the shape. - The **`serialize`** and **`deserialize`** functions inside `$serializer`, which encode/decode each property **by numeric index** (no name lookups via reflection). The plugin also adds a **`serializer()`** function (on the companion, or synthesized if absent) so you can obtain the serializer. ```kotlin @Serializable data class User(val id: Int, val name: String) // Conceptually generated by the plugin (simplified): // object User.$serializer : KSerializer<User> { // override val descriptor = buildClassSerialDescriptor("User") { // element<Int>("id"); element<String>("name") // } // override fun serialize(enc: Encoder, value: User) { // val c = enc.beginStructure(descriptor) // c.encodeIntElement(descriptor, 0, value.id) // c.encodeStringElement(descriptor, 1, value.name) // c.endStructure(descriptor) // } // override fun deserialize(dec: Decoder): User { /* loop over indices */ } // } val json = Json.encodeToString(User(1, "Ann")) // uses User.serializer() val back = Json.decodeFromString<User>(json) // reified -> serializer<User>() ``` ## Why this matters - **Performance:** no per-call reflection; encoding walks fixed indices. - **Multiplatform:** works on JVM, Native, and JS because nothing depends on JVM reflection. - **Shrinker-safe:** generated code references members directly, so R8/ProGuard can reason about them (you still keep `*$serializer` rules in some setups). - **Compile-time safety:** an unsupported field type fails the build, not at runtime. ## How you reach the generated code - `User.serializer()` — the synthesized companion function returns the `$serializer` instance. - `serializer<User>()` — a `reified` top-level helper that resolves to the same thing. - `Json.encodeToString(value)` / `decodeFromString<T>()` use these under the hood.

  • Which Gradle plugin must be applied for @Serializable to work?
    org.jetbrains.kotlin.plugin.serialization (the compiler plugin); the runtime library kotlinx-serialization-json is a separate dependency.
  • Does @Serializable produce any reflection at all?
    No reflection is needed for the generated path. The codegen produces direct calls; reflection only appears if you opt into runtime serializer lookup for non-annotated types.

Reflection is reading assembly instructions every time you build the chair; the plugin prints the finished assembly steps once at the factory.

saying these in an interview costs you the question

  • Claiming kotlinx.serialization uses runtime reflection like Gson
  • Thinking @Serializable alone works without the Gradle compiler plugin
  • Saying it works only on the JVM
  • Confusing the annotation with an annotation processor (KAPT/KSP) — it's a compiler plugin
  • Believing you must write the serializer by hand

context

open as a page

Walk through the three synthetic artifacts the kotlinx.serialization plugin generates for a @Serializable class and the role of each.

level: middleimportance: must knowfreq 55%

basics

~20 s

For each @Serializable class the plugin adds a hidden serializer object, a descriptor that lists the fields, and the functions that turn the object into data and back. Together they replace what reflection would do.

open as a page

When does the kotlinx.serialization plugin NOT generate a serializer for a @Serializable type, and what does it do instead?

level: middleimportance: should knowfreq 35%

basics

~20 s

If you already supply your own serializer, or the type is something the library handles specially (like enums or objects), the plugin skips generating the usual code and uses the provided or built-in logic instead.

open as a page

A teammate adds @Serializable to a class but Json.encodeToString fails to compile with a serializer-not-found error, or a release build crashes only after R8. As a senior, how do you diagnose codegen/build-integration issues with the serialization plugin?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Usually the compiler plugin isn't applied or the dependency is missing, so no code gets generated. For release-only crashes, a code shrinker removed or renamed the generated serializer, so you add keep rules.

open as a page

kotlinx.serialization can resolve serializers via compiler codegen or, on the JVM, via runtime reflection (e.g. Json.encodeToString(value) on an Any). Compare the two paths and their consequences.

level: seniorimportance: should knowfreq 40%

basics

~20 s

The normal path uses code the plugin built at compile time, which is fast and works everywhere. There's also a JVM-only fallback that finds the serializer with reflection at runtime, which is slower and platform-limited but more dynamic.

open as a page