skip to content

When would you choose the kotlinx.serialization compiler-plugin approach over a reflection-based library (Jackson/Gson), and what costs does compile-time codegen impose?

level: principalimportance: nice to knowfreq 30%

answer

  1. KMP/Native/JS -> only codegen works (no reflection)
  2. Codegen wins: cold start, R8 keep-rule-free, Kotlin fidelity
  3. Codegen costs: binary size, compile time, Kotlin-version coupling
  4. Jackson wins: ecosystem breadth, tree ops, foreign-type ease
  5. They can coexist per boundary

basics

~20 s

Choose kotlinx.serialization when you need Kotlin multiplatform, fast startup, and shrinker-friendly builds, or want first-class Kotlin support. The cost is more generated code, longer compiles, and a less mature ecosystem of format adapters than Jackson.

solid answer

~40 s

Pick the compiler-plugin approach when: you target **Kotlin Multiplatform** (Native/JS have no JVM reflection); you want **fast cold start** and predictable behavior under **R8/ProGuard** (no keep rules for reflected members); you value **Kotlin-native** modeling (nullability, defaults, sealed/value classes, no need for no-arg constructors). Reflection libs like Jackson remain attractive for **rich existing ecosystems** (Spring integration, vast module/databind features, JSON tree manipulation, annotations you already use) and for handling **types you can't annotate** with less ceremony. Costs of codegen: each `@Serializable` class adds generated bytecode (**binary-size** and incremental-compile impact); the plugin **couples you to the Kotlin version**; some Java-interop and dynamic scenarios are harder; the format/adapter ecosystem (e.g. exotic mappers, certain framework integrations) is smaller. Net: greenfield Kotlin/KMP → kotlinx.serialization; deep Spring/JVM-only with heavy Jackson investment → often Jackson.

go deeper

for a junior

Knows kotlinx.serialization is Kotlin-first and reflection-free; names Jackson/Gson as alternatives.

for a middle

Lists concrete benefits (multiplatform, cold start, R8) and at least one cost (version coupling or ecosystem).

for a senior

Gives a balanced decision framework and explains the shrinker and startup mechanics behind the benefits.

for a principal

Sets org-wide policy: chooses per architectural boundary, weighs binary size/compile time/version governance, and plans coexistence with reflection libraries.

## Two philosophies - **Reflection-based** (Jackson, Gson): at runtime, inspect class metadata to read/write fields. Flexible, ecosystem-rich, but pays a reflection cost and fights shrinkers and non-JVM targets. - **Compile-time codegen** (kotlinx.serialization plugin): generate `KSerializer` per `@Serializable` type during compilation; zero runtime reflection. ## Why choose the plugin - **Kotlin Multiplatform**: the decisive factor. Native and JS lack full JVM reflection; codegen is the only way to share serialization across targets. - **Startup & steady-state perf**: no reflective discovery, descriptors precomputed → better cold start (serverless/Android). - **Shrinking**: serializers reference members directly, so R8/ProGuard don't need keep rules; fewer 'works in debug, breaks in release' surprises. - **Kotlin fidelity**: defaults, nullability, sealed classes, value classes, no requirement for a no-arg constructor or mutable fields. Type discriminators are explicit and safe. - **Determinism**: behavior is fixed at compile time and visible in the descriptor. ## Costs / tradeoffs - **Binary size & compile time**: every `@Serializable` class emits a serializer + descriptor; large models grow output and can slow incremental builds. - **Kotlin-version coupling**: the plugin must track the compiler version; upgrades are coordinated. - **Ecosystem gaps**: Jackson's breadth (Spring MVC default, databind features, JSON tree/`JsonNode` ergonomics, countless modules) is larger. kotlinx has `JsonElement`/`buildJsonObject` but fewer third-party integrations. - **Foreign types**: types you can't annotate need hand-written `KSerializer` / `@Contextual` / `SerializersModule` registration — more boilerplate than Jackson's mixins/modules in some cases. - **Java interop**: it's Kotlin-first; pure-Java consumers get a clunkier experience. ## Decision guide ``` KMP or Native/JS target? -> kotlinx.serialization (plugin) Android, care about cold start/R8? -> kotlinx.serialization Deep Spring Boot + existing Jackson -> often stay on Jackson (or both) Need huge mapper ecosystem/tree ops -> Jackson Greenfield Kotlin service -> kotlinx.serialization ``` ## They can coexist A Spring Boot Kotlin service may use Jackson for HTTP message conversion while using kotlinx.serialization for a KMP-shared module or for explicit, contract-stable payloads. Choose per boundary, not globally. ## The underlying point about the plugin The compiler plugin is what *enables* the whole tradeoff: by moving serializer construction to compile time it trades a bit of build cost and version coupling for runtime speed, portability, and shrinker safety.

  • Why does compile-time codegen play nicer with R8/ProGuard than reflection?
    Generated serializers reference class members directly in bytecode, so the shrinker sees the usage and won't strip/rename them. Reflection-based libs access members by name at runtime, so you must add keep rules or risk release-only breakage.
  • Name a concrete cost of the codegen approach at scale.
    Generated serializer/descriptor code per @Serializable class increases binary size and can slow incremental compilation for very large domain models; plus the plugin couples you to the Kotlin compiler version.

saying these in an interview costs you the question

  • Claiming kotlinx.serialization is strictly better with no tradeoffs
  • Ignoring Kotlin-version coupling of the plugin
  • Not mentioning multiplatform as the decisive use case
  • Thinking you must pick exactly one library globally
  • Saying codegen has zero binary-size/compile-time cost

context