Inside a custom KSerializer, what is the contract between serialize/deserialize and the Encoder/Decoder, and how does the descriptor coordinate the two?
answer
- Encoder/Decoder are abstract; serializer issues commands
- beginStructure(descriptor) -> CompositeEncoder/Decoder
- encodeXxxElement(descriptor, index, value) keyed by index
- decodeElementIndex loop until DECODE_DONE
- Descriptor = shared schema by element index
basics
~20 sserialize 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.
solid answer
~40 sThe serializer never sees the concrete format — it issues commands to the abstract `Encoder`/`Decoder`. For primitives it calls `encodeInt`/`decodeInt`, etc. For composite types it opens a scope: `encoder.beginStructure(descriptor)` returns a `CompositeEncoder` on which you call `encodeIntElement(descriptor, index, value)` for each field, then `endStructure(descriptor)`. Decoding mirrors this: `decoder.beginStructure(descriptor)` gives a `CompositeDecoder`; you loop calling `decodeElementIndex(descriptor)` until it returns `CompositeDecoder.DECODE_DONE`, dispatching on the returned index to `decodeIntElement(descriptor, index)`. The `descriptor` is the shared contract: its element indices, names, and kinds are how the encoder/decoder and the format line up fields. The recommended high-level helpers are `encodeStructure(descriptor) { ... }` and `decodeStructure(descriptor) { ... }`, which manage begin/end for you. decodeElementIndex also enables formats to deliver fields out of order (e.g. JSON).
code
kotlin · 11 linesoverride fun deserialize(decoder: Decoder): Point =
decoder.decodeStructure(descriptor) {
var x = 0; var y = 0
while (true) when (val i = decodeElementIndex(descriptor)) {
0 -> x = decodeIntElement(descriptor, 0)
1 -> y = decodeIntElement(descriptor, 1)
CompositeDecoder.DECODE_DONE -> return@decodeStructure Point(x, y)
else -> error("Unexpected index $i")
}
@Suppress("UNREACHABLE_CODE") Point(x, y)
}go deeper
Understands serialize writes and deserialize reads, but likely not the composite/index protocol.
Can write a primitive serializer and recognizes beginStructure/encodeElement exist.
Implements the full composite encode/decode protocol with the decodeElementIndex loop and explains how the descriptor coordinates by index.
Reasons about format-order independence, decodeSequentially optimization, nested serializable delegation, and contract robustness across formats.
## The actors - **`Encoder`** — abstract sink the serializer writes to. Primitive methods (`encodeBoolean`, `encodeInt`, `encodeString`, `encodeNull`, `encodeSerializableValue`) plus `beginStructure(descriptor): CompositeEncoder` for composites. - **`Decoder`** — abstract source the deserializer reads from. Mirror methods (`decodeInt`, `decodeString`, …) plus `beginStructure(descriptor): CompositeDecoder`. - **`SerialDescriptor`** — the shared schema both sides reference by **element index**. ## Primitive flow ```kotlin override fun serialize(encoder: Encoder, value: Int) = encoder.encodeInt(value) override fun deserialize(decoder: Decoder): Int = decoder.decodeInt() ``` ## Composite flow (the important contract) Writing a multi-field type: ```kotlin override fun serialize(encoder: Encoder, value: Point) { encoder.encodeStructure(descriptor) { encodeIntElement(descriptor, 0, value.x) encodeIntElement(descriptor, 1, value.y) } } ``` `encodeStructure` calls `beginStructure(descriptor)`, runs the block on the returned `CompositeEncoder`, then `endStructure(descriptor)`. Each `encodeXxxElement` takes the **descriptor and the element index** so the format knows which named field it is. Reading mirrors it but must tolerate **any field order** and **missing optional fields**: ```kotlin override fun deserialize(decoder: Decoder): Point { return decoder.decodeStructure(descriptor) { var x = 0; var y = 0 while (true) { when (val index = decodeElementIndex(descriptor)) { 0 -> x = decodeIntElement(descriptor, 0) 1 -> y = decodeIntElement(descriptor, 1) CompositeDecoder.DECODE_DONE -> break else -> error("Unexpected index $index") } } Point(x, y) } } ``` `decodeElementIndex(descriptor)` returns the **index of the next element present** in the stream, or `DECODE_DONE` when finished. This is why JSON can present keys in any order: the decoder maps the encountered name to the descriptor index. (For strictly sequential formats, you may call `decodeSequentially()` and read in order as an optimization.) ## How the descriptor coordinates - It defines `elementsCount`, `getElementName(i)`, `getElementDescriptor(i)`, `isElementOptional(i)`, and `getElementIndex(name)`. - `encodeXxxElement`/`decodeXxxElement` are keyed by the **same indices** the descriptor exposes, so writer and reader agree without sharing concrete types. - Formats use it to translate between index and on-the-wire identity (JSON key, protobuf field number). ## Nested serializable values Use `encodeSerializableElement(descriptor, index, childSerializer, value)` and `decodeSerializableElement(...)` to delegate to another `KSerializer` for non-primitive fields. ## Pitfalls - Not handling `DECODE_DONE` -> infinite loop. - Assuming field order -> breaks for out-of-order JSON unless you opt into `decodeSequentially`. - Mismatched indices between encode and the descriptor -> wrong field names. - Forgetting `endStructure` (avoided by using `encode/decodeStructure` helpers).
- Why must deserialize loop on decodeElementIndex instead of reading fields in fixed order?Formats like JSON may deliver keys in any order and may omit optional fields; the index loop lets the decoder report which element is next, so you handle any order and detect completion via DECODE_DONE.
- What does decodeSequentially() buy you?For formats that guarantee field order (e.g. ProtoBuf), it signals you can read elements in declaration order without the index loop, a performance optimization.
- How do you serialize a field that is itself a @Serializable type?Use encodeSerializableElement(descriptor, index, ChildType.serializer(), value) and the matching decodeSerializableElement, delegating to the child serializer.
saying these in an interview costs you the question
- Calling beginStructure but never endStructure (or skipping the structure helpers)
- Reading composite fields in fixed order assuming JSON ordering
- Forgetting to break on CompositeDecoder.DECODE_DONE
- Hand-rolling format syntax instead of using Encoder/Decoder methods
- Using mismatched element indices vs the descriptor