skip to content

Serialization Core (kotlinx.serialization)

The format-independent core of kotlinx.serialization: the @Serializable plugin, the generated serializers, and the module that wires custom and polymorphic ones. It is the default serialization story for modern Kotlin, especially multiplatform.

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

explore

questions

25

In kotlinx.serialization, what does @Contextual do and why would you need it for a type like java.util.Date?

level: juniorimportance: must knowfreq 55%

answer

  1. Compile-time resolution fails for types you can't annotate
  2. @Contextual = defer to runtime lookup
  3. SerializersModule + contextual(...) = where you register
  4. Missing registration => runtime SerializationException
  5. @file:UseContextualSerialization for bulk

basics

~10 s

@Contextual tells the serializer: don't pick a serializer at compile time for this property. Look one up at runtime from a registered module. You use it for types you can't annotate, like java.util.Date.

solid answer

~40 s

kotlinx.serialization normally resolves a KSerializer at compile time via the @Serializable plugin. For a type you don't own and can't mark @Serializable (e.g. java.util.Date, java.time.Instant, BigDecimal), there is no generated serializer. @Contextual on the property (or @file:UseContextualSerialization) defers resolution: the format consults a SerializersModule at encode/decode time and uses the serializer registered there via contextual(...). If no module provides one, you get a SerializationException at runtime. So @Contextual is the 'I'll supply the serializer separately' marker; the SerializersModule is where you actually supply it. This decouples the data class from the concrete serializer, letting different formats or contexts plug in different strategies for the same type.

code

kotlin · 7 lines
kotlin
@Serializable
data class Event(val name: String, @Contextual val ts: Date)

val json = Json {
    serializersModule = SerializersModule { contextual(DateAsLongSerializer) }
}
json.encodeToString(Event("login", Date()))

go deeper

for a junior

Knows @Contextual is for types you can't annotate like Date, and that you register a serializer separately.

for a middle

Explains compile-time vs runtime resolution and that the SerializersModule supplies the serializer via contextual().

for a senior

Discusses the lost compile-time safety trade-off, @file:UseContextualSerialization, and when to prefer @Serializable(with=...) instead.

for a principal

Frames it as a strategy-injection seam: same type, different serializers per format/context; weighs runtime-failure risk against decoupling for library API design.

## The problem kotlinx.serialization works through a compiler plugin. When you annotate a class with `@Serializable`, the plugin generates a `KSerializer` for it and, for each property, resolves the serializer for that property's type at **compile time**. This works great when every type is either a built-in (Int, String, List) or itself `@Serializable`. But some types you cannot annotate: `java.util.Date`, `java.time.Instant`, `java.math.BigDecimal`, classes from a third-party JAR. You don't own the source, so you can't add `@Serializable`. The plugin then has nothing to resolve and the build fails. ## The solution: @Contextual `@Contextual` is an annotation you place on a property whose type has no compile-time serializer: ```kotlin import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable import java.util.Date @Serializable data class Event( val name: String, @Contextual val timestamp: Date, ) ``` It instructs the generated serializer to emit a **ContextualSerializer** for that slot instead of a concrete one. At runtime, that ContextualSerializer asks the active **SerializersModule** for a serializer registered for `Date::class`. Resolution is therefore deferred from compile time to **encode/decode time**. ## Supplying the serializer: SerializersModule You must write a `KSerializer<Date>` and register it: ```kotlin import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.json.Json val module = SerializersModule { contextual(DateAsLongSerializer) } val json = Json { serializersModule = module } ``` Now `json.encodeToString(Event(...))` works. If you forget to register, you get a `SerializationException: Serializer for class 'Date' is not found` **at runtime**, not at compile time — that loss of compile-time safety is the trade-off. ## File-level alternative If many properties use the same type, annotating each is noisy. Use the file-level annotation: ```kotlin @file:UseContextualSerialization(Date::class) ``` Every `Date` property in the file is then treated as contextual without per-property `@Contextual`. ## Key terms - **KSerializer<T>**: the interface with `serialize`/`deserialize` and a `descriptor`. - **SerializersModule**: a runtime registry mapping classes to serializers. - **contextual(...)**: the DSL function that registers a serializer for a class in a module. - **ContextualSerializer**: the placeholder serializer the plugin emits for `@Contextual` slots; it looks up the real one in the module.

  • What happens if you mark a property @Contextual but never register a serializer for its type?
    Encoding/decoding throws a SerializationException at runtime saying the serializer for that class was not found. There is no compile-time error.
  • Could you avoid @Contextual for Date entirely?
    Yes — write a KSerializer<Date> and pass it directly with @Serializable(with = DateSerializer::class), which resolves at compile time. @Contextual is for when you want runtime/module-based resolution instead.

@Contextual is an IOU: 'I owe you a serializer for this type — collect it from the module at runtime.'

saying these in an interview costs you the question

  • Thinks @Contextual itself provides the serialization logic
  • Believes a missing registration fails at compile time
  • Confuses @Contextual with @Transient (skipping a field)
  • Cannot name SerializersModule or contextual() as the registration mechanism

context

open as a page

In kotlinx.serialization, what is a KSerializer<T> and what are the three things every serializer must provide?

level: juniorimportance: must knowfreq 65%

basics

~20 s

A KSerializer<T> is the object that knows how to turn a value of type T into a stream of data and read it back. It provides a serialize function, a deserialize function, and a descriptor describing the type.

open as a page

How do you make a kotlinx.serialization property serialize under a different JSON key than its Kotlin name, and what is the role of @SerialName?

level: juniorimportance: must knowfreq 75%

basics

~10 s

Put @SerialName("json_key") on the property. The Kotlin field keeps its name in code, but the JSON uses the name you give. Useful when JSON uses snake_case but Kotlin uses camelCase.

open as a page

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%

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.

open as a page

What is a SerializersModule in kotlinx.serialization, and when do you need one?

level: juniorimportance: must knowfreq 55%

basics

~10 s

A SerializersModule is a registry that tells the serialization library how to handle types it cannot figure out on its own, like interfaces or open base classes with many possible subtypes.

open as a page

Show how to write a KSerializer for java.util.Date and register it so a @Contextual property serializes. What exactly does contextual() bind?

level: middleimportance: must knowfreq 48%

basics

~10 s

Write a class implementing KSerializer<Date> with serialize, deserialize and a descriptor. Then build a SerializersModule with contextual(YourSerializer) and pass it to Json. contextual() binds Date::class to that serializer.

open as a page

What are the ways to obtain a KSerializer instance in kotlinx.serialization, and how do Type.serializer() and the top-level serializer<T>() differ?

level: middleimportance: must knowfreq 55%

basics

~10 s

You can call the generated MyType.serializer() function on a @Serializable class, or use the top-level serializer<T>() / serializer(KType) functions to look one up, including for generic types like List<User>.

open as a page

What does @Transient do in kotlinx.serialization, and why must such a property have a default value?

level: middleimportance: must knowfreq 65%

basics

~20 s

@Transient tells the serializer to skip a property entirely: it is not written to JSON and not read from it. Because it is never decoded, it must have a default value so an object can still be constructed.

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

How do you register an open class hierarchy for polymorphic serialization using the polymorphic { subclass() } builder, and what JSON does it produce?

level: middleimportance: must knowfreq 50%

basics

~10 s

Inside SerializersModule { } you call polymorphic(Base::class) { subclass(Impl::class) } for each implementation. The output JSON gets an extra type field naming which subclass it is, so decoding can pick the right one.

open as a page

Compare @Contextual, @Serializable(with = ...), and @file:UseContextualSerialization. When does each resolve, and which one would you choose?

level: middleimportance: should knowfreq 40%

basics

~10 s

@Serializable(with=...) hard-wires one serializer at compile time. @Contextual defers to a runtime module so it can vary. @file:UseContextualSerialization is just @Contextual applied to every use of a type in a file.

open as a page

Why does KSerializer expose a SerialDescriptor rather than relying on Kotlin reflection at runtime, and what does the descriptor's kind tell a format?

level: middleimportance: should knowfreq 30%

basics

~20 s

The descriptor is a lightweight, precomputed description of a type's structure, generated at compile time so the library doesn't need slow runtime reflection. Its kind tells the format whether the value is a primitive, an object, a list, a map, and so on.

open as a page

By default kotlinx.serialization treats a property with a default value as optional during decoding. How does @Required change that, and when would you use it?

level: middleimportance: should knowfreq 50%

basics

~20 s

Properties with a default value can be left out of the JSON and the default fills in. @Required forces that property to be present in the input even though it has a default — decoding fails if it's missing.

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

Why do sealed hierarchies serialize polymorphically without a SerializersModule, while interfaces and open classes require explicit registration?

level: middleimportance: should knowfreq 42%

basics

~20 s

A sealed type lists all its subtypes in one place, so the compiler already knows them and wires them up. Open classes and interfaces can be extended anywhere, so the compiler can't list them and you must register subtypes yourself.

open as a page

Explain exactly how a @Contextual property is resolved at encode time, and what failure modes and edge cases (nullability, generics, missing registration) you should anticipate.

level: seniorimportance: should knowfreq 30%

basics

~10 s

At encode time the ContextualSerializer asks the format's module for a serializer matching the property's class. If none is found it throws. Nullable types and generic types need extra care so the lookup matches.

open as a page

How do you implement a custom KSerializer<T> for a value class that should serialize as a primitive (e.g. a Color stored as an Int hex code serialized as a String)? Walk through serialize, deserialize, and descriptor.

level: seniorimportance: should knowfreq 45%

basics

~20 s

Write a class implementing KSerializer<Color>. Give it a primitive descriptor (a String kind). In serialize, convert the Color to a String and call encodeString. In deserialize, call decodeString and parse it back into a Color.

open as a page

Inside a custom KSerializer, what is the contract between serialize/deserialize and the Encoder/Decoder, and how does the descriptor coordinate the two?

level: seniorimportance: should knowfreq 35%

basics

~20 s

serialize writes data by calling methods on the Encoder; deserialize reads it back by calling methods on the Decoder. The descriptor describes the structure so both sides agree on names, order, and types of fields.

open as a page

You receive a partner API: keys are snake_case, an 'apiVersion' must always be present in every payload you send, an internal 'requestTrace' must never leave your process, and a 'currency' field has a sensible default but must be present in inbound requests. Which kotlinx.serialization annotations/settings do you apply to each?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Rename keys with @SerialName (or a snake_case naming strategy); force apiVersion out with @EncodeDefault(ALWAYS); hide requestTrace with @Transient; make currency mandatory on input with @Required while keeping its default.

open as a page

Explain @EncodeDefault and the encodeDefaults Json setting. How do you ensure (or prevent) default-valued properties appearing in the output?

level: seniorimportance: should knowfreq 45%

basics

~10 s

By default kotlinx-json writes out properties even when they equal their default. encodeDefaults=false drops them. @EncodeDefault overrides this per property: ALWAYS forces it written, NEVER forces it skipped, regardless of the global setting.

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

How do you handle unknown or unregistered subtypes in a polymorphic SerializersModule, and what are the failure modes?

level: seniorimportance: should knowfreq 35%

basics

~10 s

By default, an unknown type throws an error. You can register a fallback with defaultDeserializer for decoding and default for encoding, so unexpected types map to a safe placeholder instead of crashing.

open as a page

How are SerializersModules composed, scoped to a format, and combined when multiple libraries each contribute one?

level: seniorimportance: nice to knowfreq 24%

basics

~10 s

You can merge several modules into one with the plus operator, attach the result to a format like Json, and use overwriteWith when two modules register the same type and one should win.

open as a page

As a principal engineer, when would you mandate @Contextual over @Serializable(with=...) across a large/multiplatform codebase, and what are the systemic risks and governance practices?

level: principalimportance: nice to knowfreq 16%

basics

~20 s

Mandate @Contextual when the same type must serialize differently per platform, format, or consumer, or when a library wants pluggable serializers. Otherwise prefer the compile-time with=. The big risk is runtime failures from a missing or wrong module.

open as a page