Hand-write serialize/deserialize for a structured type using beginStructure/endStructure and decodeElementIndex. Why is the decodeElementIndex loop necessary and how do you handle CompositeDecoder.DECODE_DONE?
answer
- buildClassSerialDescriptor { element<T>(name) } sets indices
- encodeStructure / decodeStructure = begin+end with safety
- loop decodeElementIndex until DECODE_DONE (-1)
- order not guaranteed; optionals may be skipped
- bitmask to detect missing required fields
basics
~20 sYou open a structure, write each field with its index, then close it. To read, you loop asking the decoder which field comes next until it says done. The loop is needed because formats may send fields in any order or skip optional ones.
solid answer
~40 sFor full manual control, build the descriptor with `buildClassSerialDescriptor` (declaring each element), then in `serialize` call `encoder.beginStructure(descriptor)` to get a `CompositeEncoder`, write each field via `encodeIntElement(descriptor, index, value)` etc., and `endStructure(descriptor)`. In `deserialize` call `decoder.beginStructure(descriptor)` to get a `CompositeDecoder`, then loop: `when (val i = decodeElementIndex(descriptor)) { 0 -> ...; 1 -> ...; CompositeDecoder.DECODE_DONE -> break; else -> error }`. The loop exists because formats may deliver elements out of order (JSON object keys), omit optional/default fields, or be streaming — you can't assume a fixed order. `DECODE_DONE` (value -1) signals no more elements; you break and then `endStructure`. Track required fields with a bitmask/flags to detect missing ones.
code
kotlin · 13 linesoverride fun deserialize(decoder: Decoder): Point =
decoder.decodeStructure(descriptor) {
var x = 0.0; var y = 0.0
loop@ while (true) {
when (val i = decodeElementIndex(descriptor)) {
0 -> x = decodeDoubleElement(descriptor, 0)
1 -> y = decodeDoubleElement(descriptor, 1)
CompositeDecoder.DECODE_DONE -> break@loop
else -> error("Unexpected index $i")
}
}
Point(x, y)
}go deeper
Can recognize beginStructure/endStructure but typically needs the surrogate pattern instead.
Writes the loop and handles DECODE_DONE, using decodeStructure sugar.
Explains why the loop is mandatory (order/optionals/streaming), tracks missing fields with a bitmask, knows decodeSequentially.
Weighs manual encoding vs surrogate for allocation/perf and reasons about UNKNOWN_NAME handling and format-config interplay.
## When you need this The surrogate pattern covers most structured cases. You drop to the raw `CompositeEncoder`/`CompositeDecoder` API when you need custom field logic, conditional fields, or to avoid an extra surrogate allocation. ## Building the descriptor ```kotlin override val descriptor: SerialDescriptor = buildClassSerialDescriptor("com.acme.Point") { element<Double>("x") element<Double>("y") } ``` Element order here defines the **indices** (0 = x, 1 = y) used everywhere else. ## Serializing ```kotlin override fun serialize(encoder: Encoder, value: Point) { encoder.encodeStructure(descriptor) { // begin + auto end encodeDoubleElement(descriptor, 0, value.x) encodeDoubleElement(descriptor, 1, value.y) } } ``` `encodeStructure { }` is sugar over `beginStructure` + `endStructure` with exception safety. ## Deserializing — the index loop ```kotlin override fun deserialize(decoder: Decoder): Point = decoder.decodeStructure(descriptor) { var x = Double.NaN; var y = Double.NaN var seen = 0 while (true) { when (val i = decodeElementIndex(descriptor)) { 0 -> { x = decodeDoubleElement(descriptor, 0); seen = seen or 1 } 1 -> { y = decodeDoubleElement(descriptor, 1); seen = seen or 2 } CompositeDecoder.DECODE_DONE -> break else -> error("Unexpected index $i") } } require(seen == 3) { "Missing fields" } Point(x, y) } ``` ## Why the loop is mandatory - **Order isn't guaranteed.** JSON object keys can arrive in any order; the decoder reports the *actual* next element via its index, not a fixed sequence. - **Optional/default fields** may be absent — the decoder simply never reports their index. - **Streaming formats** read incrementally; the loop reads until exhausted. Reading fields positionally without the loop breaks on reordered or missing keys. ## DECODE_DONE `CompositeDecoder.DECODE_DONE` is the sentinel (value `-1`) meaning 'no more elements'. You `break` out of the loop on it, then `endStructure` (handled automatically by `decodeStructure`). There's also `UNKNOWN_NAME` for keys not in the descriptor; how it's reported depends on format config (e.g. JSON `ignoreUnknownKeys`). ## Detecting missing fields Use a bitmask (`seen`) to verify all required elements arrived; throw `MissingFieldException`/`require` otherwise. The generated serializers do exactly this under the hood.
- Why not just call decodeDoubleElement for index 0 then 1 without the loop?Because the format may deliver elements out of order or omit optionals. The loop reads whatever index the decoder reports next, which is the only correct contract.
- What is decodeSequentially() and when can it skip the loop?Some formats guarantee in-order, complete delivery; decodeSequentially() returns true and lets you read elements positionally for speed. Only valid when the format/decoder guarantees it.
saying these in an interview costs you the question
- Reading fields positionally without the decodeElementIndex loop
- Forgetting to break on DECODE_DONE (infinite loop)
- Not calling endStructure (or not using decodeStructure)
- Assuming JSON keys arrive in declaration order
- Ignoring missing required fields instead of tracking a bitmask