skip to content

Write a custom KSerializer for a value type using PrimitiveSerialDescriptor. Why must the descriptor's serialName be unique, and what determines the PrimitiveKind?

level: middleimportance: must knowfreq 55%

answer

  1. PrimitiveSerialDescriptor(name, PrimitiveKind)
  2. serialName unique — often the FQ class name
  3. kind must match encodeString/encodeLong call
  4. wrong name => bad polymorphic dispatch
  5. scalar => primitive descriptor, not buildClassSerialDescriptor

basics

~20 s

Use PrimitiveSerialDescriptor with a unique name and the basic kind (string, long, etc.) your wire value has. The name must be unique so the format can tell types apart; the kind matches the encode/decode call you actually use.

solid answer

~40 s

For a type that maps to a single scalar, use `PrimitiveSerialDescriptor(serialName, kind)`. The `serialName` is a globally identifying string — it must be unique across all descriptors because formats (and polymorphism/registries) key on it; reusing a name can cause collisions or wrong dispatch. The `PrimitiveKind` (STRING, LONG, INT, BOOLEAN, DOUBLE, etc.) must match the encoder call you make: if `serialize` calls `encoder.encodeString(...)`, the kind must be `STRING`; if it calls `encodeLong`, use `LONG`. Mismatches lead to format errors or incorrect schemas (e.g. ProtoBuf field types). The descriptor is created once (often as a property) and reused; it should never be a full `buildClassSerialDescriptor` for a scalar — that would model it as an object.

code

kotlin · 8 lines
kotlin
object UserIdSerializer : KSerializer<UserId> {
    override val descriptor =
        PrimitiveSerialDescriptor("com.acme.UserId", PrimitiveKind.LONG)
    override fun serialize(encoder: Encoder, value: UserId) =
        encoder.encodeLong(value.raw)
    override fun deserialize(decoder: Decoder): UserId =
        UserId(decoder.decodeLong())
}

go deeper

for a junior

Can call PrimitiveSerialDescriptor with a name and kind and a matching encode call.

for a middle

Explains why the name must be unique and why kind must match the encoder method.

for a senior

Ties serialName uniqueness to polymorphic dispatch and self-describing formats; uses FQ-name convention.

for a principal

Treats the descriptor as part of the public wire schema and reasons about its impact on ProtoBuf/CBOR tooling and evolution.

## PrimitiveSerialDescriptor When your type is conceptually a single scalar (a wrapped String, an ID that's really a Long), its `SerialDescriptor` should be **primitive**. You build it with: ```kotlin PrimitiveSerialDescriptor(serialName: String, kind: PrimitiveKind) ``` `PrimitiveKind` is one of `BOOLEAN, BYTE, CHAR, SHORT, INT, LONG, FLOAT, DOUBLE, STRING`. ## serialName must be unique The `serialName` is the descriptor's identity. It matters because: - **Polymorphism & registries** use it as the type discriminator key. Two distinct serializers sharing a name can collide in a `SerializersModule` or sealed-class registry. - **Self-describing formats** (e.g. ProtoBuf schema generation, debugging) print it. - A common convention is the fully-qualified class name (e.g. `"com.acme.Color"`) to guarantee uniqueness. Reusing a name across two unrelated types is a latent bug — most often it surfaces as wrong polymorphic dispatch, not an immediate crash. ## PrimitiveKind must match the encode/decode calls The descriptor declares the *shape*; the `serialize`/`deserialize` bodies must agree: ```kotlin object ColorAsHexSerializer : KSerializer<Color> { override val descriptor = PrimitiveSerialDescriptor("com.acme.Color", PrimitiveKind.STRING) override fun serialize(encoder: Encoder, value: Color) { encoder.encodeString("#%06x".format(value.rgb)) // STRING -> encodeString } override fun deserialize(decoder: Decoder): Color { return Color(decoder.decodeString().removePrefix("#").toInt(16)) } } ``` If the kind were `LONG` but you called `encodeString`, formats that rely on the descriptor (ProtoBuf, CBOR) would build a wrong schema or fail. JSON is more forgiving at runtime but the descriptor still drives things like ProtoBuf field types and tooling. ## Don't model a scalar as a class Using `buildClassSerialDescriptor` for a scalar would wrap it in an object `{ ... }`. For a single value, `PrimitiveSerialDescriptor` keeps it a bare scalar on the wire.

  • What happens if descriptor kind is LONG but serialize calls encodeString?
    The descriptor and behavior disagree. Schema-driven formats (ProtoBuf/CBOR) build a wrong or invalid schema; it's a bug even if JSON happens to tolerate it. Keep them consistent.
  • What's a safe convention for serialName?
    Use the fully-qualified type name so it's globally unique and won't collide in polymorphic registries.

saying these in an interview costs you the question

  • Hardcoding a non-unique serialName like "value"
  • Mismatching PrimitiveKind with the encode call
  • Using buildClassSerialDescriptor for a single scalar
  • Thinking serialName is cosmetic and never matters
  • Recreating the descriptor on every serialize call instead of once

context