skip to content

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%

answer

  1. Apply kotlin('plugin.serialization'), match Kotlin version
  2. Need the runtime json/core dependency too
  3. Erased Any / unsupplied generic args = compile failure
  4. All property types must be serializable/contextual
  5. Release-only crash = R8 stripped $serializer -> keep rules

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.

solid answer

~40 s

Two failure families. **Compile-time "serializer not found":** the **Gradle compiler plugin** `org.jetbrains.kotlin.plugin.serialization` isn't applied (or its version is mismatched with the Kotlin version), or the runtime dependency (`kotlinx-serialization-json`) is absent — so no `$serializer` is generated/resolved. Other causes: serializing an **erased/`Any`** type (no compile-time serializer), a generic without supplied type-argument serializers, or a property whose type isn't `@Serializable` and has no contextual/custom serializer. **Runtime-only after R8/ProGuard:** the shrinker stripped or renamed `$serializer` companions, so reflective lookup fails — add **keep rules** (keep `@Serializable` classes' `**$serializer` and companions). Diagnose by checking: plugin applied + versions aligned, dependency present, the static type is concrete, all nested types serializable, and consult-mapping/keep rules for release. The codegen path should fail at compile time; if it only fails in release, suspect shrinking.

code

kotlin · 11 lines
kotlin
// build.gradle.kts
plugins {
    kotlin("jvm") version "2.0.20"
    kotlin("plugin.serialization") version "2.0.20" // version MUST equal kotlin version
}
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
}

@Serializable class Box<T>(val value: T)
// Requires an element serializer: Box.serializer(User.serializer())

go deeper

for a junior

Can spot that the dependency or plugin might be missing and add it.

for a middle

Distinguishes missing-plugin/dependency from erased-type issues and checks property serializability.

for a senior

Systematically separates compile-time vs release-only failures, knows version-matching, generic element serializers, and R8 keep rules.

for a principal

Builds the team's serialization build conventions, codifies keep rules, and reasons about why codegen pushes errors to compile time as a reliability property.

## Family 1 — compile-time: "Serializer for class 'X' is not found" The plugin couldn't wire a serializer at compile time. Checklist: - **Compiler plugin not applied.** You need the Gradle plugin, not just the library: ```kotlin plugins { kotlin("jvm") version "2.x" kotlin("plugin.serialization") version "2.x" // MUST match Kotlin version } dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.x") } ``` The plugin **version must match the Kotlin version** (it's a compiler plugin tied to compiler internals). A mismatch can fail the build or silently skip generation. - **Missing runtime dependency.** The plugin generates code referencing `kotlinx-serialization-core`/`-json`; without it, references don't resolve. - **Erased static type.** `val x: Any = User(...); Json.encodeToString(x)` — no compile-time serializer; pass a reified `T` or explicit serializer. - **Generic type without element serializers.** A `@Serializable class Box<T>(val t: T)` needs `Box.serializer(elementSerializer)`; the plugin generates a `serializer(vararg KSerializer<*>)` form for generics. - **A property's type isn't serializable.** Every property type must itself be `@Serializable`, a built-in, `@Contextual`, or have a custom serializer — otherwise the build fails pointing at that type. - **`@Transient` field without a default** is a separate compile error (must have a default), worth recognizing. ## Family 2 — runtime-only, after R8/ProGuard/minify It compiled and works in debug but the **release** build throws `SerializationException`/`NoSuchMethodError`/`ClassNotFoundException` for `$serializer`. The shrinker removed or renamed the generated companion/`$serializer` that runtime lookup expects. Fixes: ```proguard # Keep generated serializers and companions -keepclassmembers class **$$serializer { *; } -keepclasseswithmembers class * { @kotlinx.serialization.Serializable <fields>; } -keep,includedescriptorclasses class **$$serializer { *; } ``` (The exact rules ship with recent kotlinx-serialization; the principle: don't strip/rename `*$serializer` and the companion `serializer()` if anything resolves serializers reflectively at runtime.) ## Diagnostic flow 1. Is `kotlin("plugin.serialization")` applied **and** version-matched to Kotlin? 2. Is `kotlinx-serialization-json` (or the format lib) a dependency? 3. Is the **static type concrete** at the call site (not `Any`/erased)? Use reified/explicit serializer. 4. Are **all nested/property types** serializable/contextual? 5. For generics, are **type-argument serializers** supplied? 6. If only release fails → **R8/ProGuard keep rules** for `*$serializer`/companions; check `mapping.txt`. ## Principle The codegen design means most problems surface as **compile errors** — that's the feature. A failure appearing **only at runtime/release** almost always points to the reflective fallback plus shrinking, not the codegen itself.

  • Why must the serialization plugin version match the Kotlin version?
    It's a compiler plugin operating on compiler IR internals, which change between Kotlin versions; a mismatch can fail the build or skip codegen.
  • Why does a build pass in debug but crash only in release?
    R8/ProGuard minification can remove or rename the generated $serializer/companion that runtime reflective lookup needs; keep rules prevent that.

saying these in an interview costs you the question

  • Adding only the dependency but forgetting the Gradle compiler plugin
  • Using a plugin version that doesn't match the Kotlin version
  • Ignoring R8 keep rules for release builds
  • Serializing erased Any and blaming the library
  • Not realizing generic @Serializable types need element serializers

context