skip to content

KSerializer & Generated Serializers

KSerializer pairs a serialize and deserialize implementation with a descriptor that describes the shape, and you obtain one with Type.serializer() or serializer<T>(). Understanding this interface is the prerequisite for writing a custom serializer.

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

questions

5

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

level: juniorimportance: must knowfreq 65%

answer

  1. serialize + deserialize + descriptor
  2. Extends SerializationStrategy and DeserializationStrategy
  3. Talks to abstract Encoder/Decoder, not a format
  4. descriptor = SerialDescriptor metadata
  5. Type.serializer() is plugin-generated

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.

solid answer

~40 s

KSerializer<T> is the central interface in kotlinx.serialization that defines how a type T is converted to and from a format-agnostic stream. It combines two parent interfaces: SerializationStrategy<T> (serialize(encoder: Encoder, value: T)) and DeserializationStrategy<T> (deserialize(decoder: Decoder): T). It also exposes a SerialDescriptor (the descriptor property) describing the type's structure — its serial name, kind (e.g. CLASS, LIST), and elements. The serializer is format-independent: the same KSerializer works with Json, ProtoBuf, Cbor, etc., because it talks to the abstract Encoder/Decoder, not to a concrete format. Most serializers are generated by the compiler plugin from @Serializable, but you can implement KSerializer yourself for custom logic.

code

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

val serializer: KSerializer<User> = User.serializer()
println(serializer.descriptor.serialName) // "User"
println(serializer.descriptor.kind)       // CLASS

val json = Json.encodeToString(serializer, User(1, "Ann"))
val back = Json.decodeFromString(serializer, json)

go deeper

for a junior

Names the three members and knows serializers usually come from @Serializable via Type.serializer().

for a middle

Explains the format-agnostic design and that serialize/deserialize use Encoder/Decoder while descriptor carries metadata.

for a senior

Articulates the separation of type-shape (serializer) from byte-production (format) and how descriptor kinds drive format behavior.

for a principal

Frames KSerializer as the contract enabling multi-format support and discusses trade-offs of plugin codegen vs hand-written serializers across a codebase.

## What KSerializer<T> is `KSerializer<T>` is the core abstraction of **kotlinx.serialization**: a single object that knows how to encode a value of type `T` into a generic stream and decode it back. "Generic" is the key word — a serializer is **format-agnostic**. The same `KSerializer<User>` is used whether you ultimately produce JSON, CBOR, or Protobuf, because it never talks to a concrete format. It talks to the abstract `Encoder` and `Decoder` interfaces, and the format implementation (e.g. `Json`) supplies the concrete encoder/decoder. ## The three members `KSerializer<T>` extends two interfaces and adds a property: - **`serialize(encoder: Encoder, value: T)`** — from `SerializationStrategy<T>`. Writes `value` by calling methods on the `Encoder` (e.g. `encodeString`, `encodeInt`, or `beginStructure`). - **`deserialize(decoder: Decoder): T`** — from `DeserializationStrategy<T>`. Reads from the `Decoder` and reconstructs a `T`. - **`val descriptor: SerialDescriptor`** — metadata about the type: its `serialName`, its `kind` (`StructureKind.CLASS`, `StructureKind.LIST`, `PrimitiveKind.STRING`, …), the number and names of its elements, and which are optional/nullable. Formats use the descriptor to drive structure (e.g. JSON object keys). ```kotlin interface KSerializer<T> : SerializationStrategy<T>, DeserializationStrategy<T> { override val descriptor: SerialDescriptor } ``` ## Where serializers come from - For a class marked `@Serializable`, the **compiler plugin generates** a serializer, reachable via the synthetic `User.serializer()` companion-like function. - Built-in types (`Int`, `String`, `List`, …) have library-provided serializers. - You can write your own by implementing `KSerializer<T>` directly — typically delegating the descriptor to a primitive or wrapping another serializer. ## Why it matters Because everything funnels through `KSerializer` + `Encoder`/`Decoder`, the design cleanly separates **what** a type looks like (the serializer/descriptor) from **how** bytes are produced (the format). That is why one annotation, `@Serializable`, makes a class work across every format.

  • Why is KSerializer not tied to a specific format like JSON?
    Because serialize/deserialize operate on the abstract Encoder/Decoder interfaces; the concrete format (Json, ProtoBuf) supplies those, so one serializer works everywhere.
  • What does the descriptor add beyond serialize/deserialize?
    Structural metadata — serial name, kind, element names, optionality — which formats use to lay out output and to validate/skip during decoding.

A KSerializer is like a translator who knows a language's grammar (descriptor) and can both speak it (serialize) and understand it (deserialize) — independent of whether the conversation happens by phone or letter (the format).

saying these in an interview costs you the question

  • Saying KSerializer produces JSON directly (it produces nothing format-specific; the format does)
  • Forgetting the descriptor and listing only serialize/deserialize
  • Claiming you must always write KSerializer by hand
  • Confusing KSerializer with Json or with @Serializable itself

context

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

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

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