In kotlinx.serialization, what is a custom KSerializer and when would you write one by hand instead of relying on the @Serializable-generated serializer?
answer
- descriptor + serialize + deserialize = the three members
- @Serializable generates one; hand-write when default is wrong
- third-party type / different wire shape / validation
- @Serializable(with=...) or @file:UseSerializers
- format-agnostic: works for JSON, ProtoBuf, CBOR
basics
~20 sIt is a small class that tells the library exactly how to turn one type into data and back. You write one when the default behavior is wrong, for types you cannot annotate, or when the wire format must differ from your class.
solid answer
~40 sA KSerializer<T> is an object implementing three things: a descriptor (the type's shape/name for the format), serialize(encoder, value), and deserialize(decoder). For a plain @Serializable class the compiler plugin generates one automatically. You hand-write one when: the class comes from a third-party library you cannot annotate; the JSON/binary shape must differ from the Kotlin class (e.g. a date stored as an epoch Long but modeled as Instant); or you need to validate/transform during (de)serialization. You attach it with @Serializable(with = MySerializer::class) on the property or class, or register it at file level via @file:UseSerializers(MySerializer::class). The serializer is format-agnostic: the same KSerializer works for JSON, ProtoBuf, CBOR.
code
kotlin · 8 linesobject InstantSerializer : KSerializer<Instant> {
override val descriptor =
PrimitiveSerialDescriptor("Instant", PrimitiveKind.LONG)
override fun serialize(encoder: Encoder, value: Instant) =
encoder.encodeLong(value.epochSecond)
override fun deserialize(decoder: Decoder): Instant =
Instant.ofEpochSecond(decoder.decodeLong())
}go deeper
Knows a KSerializer has descriptor, serialize, deserialize and that @Serializable usually generates it.
Can list concrete reasons to hand-write one (third-party type, different shape, validation) and wire it via @Serializable(with=...).
Stresses format-agnosticism and decoupling the wire contract from the model; reaches for @file:UseSerializers at scale.
Frames custom serializers as a stable API boundary and weighs them against schema-evolution and polymorphism strategies.
## What a KSerializer is kotlinx.serialization splits work into two halves: a **format** (Json, ProtoBuf, Cbor) that knows how to write bytes, and a **serializer** that knows the *structure* of a specific type. A `KSerializer<T>` is the per-type strategy. The interface has three members: - `descriptor: SerialDescriptor` — metadata describing the type's shape (kind, name, element names). Formats read this to know how many fields exist and what to call them. - `serialize(encoder: Encoder, value: T)` — writes `value` into the supplied `Encoder`. - `deserialize(decoder: Decoder): T` — reads a `T` back from the `Decoder`. ## Why the compiler usually does it for you When you put `@Serializable` on a class, the **compiler plugin** generates a nested serializer (`MyClass.serializer()`) plus the descriptor automatically. You almost never see it. ## When to hand-write one You write a custom `KSerializer` when the generated one is unavailable or wrong: - **Third-party type** you can't annotate (e.g. `java.time.Instant`, a class from a dependency). - **Format differs from the model** — your class is `Color(r,g,b)` but the JSON must be the string `"#ff0000"`. - **Validation/normalization** — reject out-of-range values, trim strings, enforce invariants while decoding. - **Stable wire contract** — you want the serialized form decoupled from internal refactors. ## Wiring it in ```kotlin // Per-use, on a property or the class itself: @Serializable data class Event( @Serializable(with = InstantSerializer::class) val at: Instant, ) // File-wide, so every Instant in the file uses it: @file:UseSerializers(InstantSerializer::class) ``` A hand-written serializer is **format-agnostic** — the same `InstantSerializer` is used whether the format is JSON, ProtoBuf or CBOR, because it talks to the abstract `Encoder`/`Decoder`, not to JSON directly.
- Does a custom KSerializer only work for JSON?No. It talks to the abstract Encoder/Decoder, so the same serializer works for any format — JSON, ProtoBuf, CBOR, etc.
- What's the difference between @Serializable(with=...) and @file:UseSerializers?@Serializable(with=...) applies to a single class or property; @file:UseSerializers registers serializers for all occurrences of those types within the file.
A KSerializer is a translator hired for one specific language pair; the format is the messenger who carries whatever the translator produces.
saying these in an interview costs you the question
- Thinking a custom serializer is JSON-specific
- Believing you must annotate every class with @Serializable to serialize it
- Confusing the format (Json) with the serializer (per-type strategy)
- Not knowing the three members descriptor/serialize/deserialize
- Claiming reflection is used at runtime (it's compiler-generated)