skip to content

Show how to write a KSerializer for java.util.Date and register it so a @Contextual property serializes. What exactly does contextual() bind?

level: middleimportance: must knowfreq 48%

answer

  1. KSerializer = descriptor + serialize + deserialize
  2. Date.time is epoch millis (Long)
  3. contextual(serializer) infers KClass from generic
  4. Module attaches to the format instance (Json { serializersModule })
  5. Provider lambda overload handles generic types

basics

~10 s

Write a class implementing KSerializer<Date> with serialize, deserialize and a descriptor. Then build a SerializersModule with contextual(YourSerializer) and pass it to Json. contextual() binds Date::class to that serializer.

solid answer

~40 s

You implement KSerializer<Date>: override descriptor (e.g. PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG)), serialize() calling encoder.encodeLong(value.time), and deserialize() reading decoder.decodeLong() into Date(...). Register it in a SerializersModule via contextual(DateAsLongSerializer); the contextual() overload infers Date::class from the serializer's generic type, binding that KClass to the serializer instance. Attach the module to the format: Json { serializersModule = module }. At runtime the ContextualSerializer emitted for the @Contextual Date slot queries this module by KClass and delegates. contextual() can also take an explicit KClass and even a lambda (typeArgs -> serializer) for parameterized types. The binding is per-format-instance: a different Json with a different module can serialize the same Date type differently — that's the whole point of contextual resolution.

code

kotlin · 7 lines
kotlin
object DateAsLongSerializer : KSerializer<Date> {
    override val descriptor = PrimitiveSerialDescriptor("java.util.Date", PrimitiveKind.LONG)
    override fun serialize(e: Encoder, v: Date) = e.encodeLong(v.time)
    override fun deserialize(d: Decoder) = Date(d.decodeLong())
}

val json = Json { serializersModule = SerializersModule { contextual(DateAsLongSerializer) } }

go deeper

for a junior

Can copy the three KSerializer methods and call contextual(serializer) without fully explaining descriptor mechanics.

for a middle

Writes the serializer correctly, knows contextual() infers the KClass, and attaches the module to the format.

for a senior

Explains descriptor naming pitfalls, the provider-lambda overload for generics, and encode/decode module symmetry.

for a principal

Designs a reusable serializers-module strategy across formats, considers descriptor stability for schema/compat, and library packaging of modules.

## Writing the serializer A `KSerializer<T>` needs three things: a `descriptor` (metadata describing the wire shape), a `serialize` method, and a `deserialize` method. ```kotlin import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import java.util.Date object DateAsLongSerializer : KSerializer<Date> { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("java.util.Date", PrimitiveKind.LONG) override fun serialize(encoder: Encoder, value: Date) = encoder.encodeLong(value.time) override fun deserialize(decoder: Decoder): Date = Date(decoder.decodeLong()) } ``` - **descriptor**: declares this is a single LONG primitive. The serial name should be unique; reusing a built-in name can cause clashes in polymorphic/contextual registries. - **serialize**: `Date.time` is epoch millis (a Long) → `encoder.encodeLong`. - **deserialize**: read the Long, wrap back into `Date`. ## Registering it ```kotlin import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.contextual import kotlinx.serialization.json.Json val module = SerializersModule { contextual(DateAsLongSerializer) } val json = Json { serializersModule = module } ``` ## What contextual() binds The `contextual(serializer)` overload infers the `KClass` from the serializer's generic parameter (`KSerializer<Date>` → `Date::class`) and stores the mapping `Date::class -> DateAsLongSerializer` in the module. Other overloads: ```kotlin contextual(Date::class, DateAsLongSerializer) // explicit KClass contextual(MyBox::class) { args -> MyBoxSerializer(args[0]) } // provider for generics ``` The provider lambda receives the serializers for the type arguments, which is how you support parameterized contextual types like `Box<T>`. ## Why it's per-format The module is attached to a specific `Json` (or `Cbor`, `ProtoBuf`) instance. Two different formats can register **different** serializers for `Date` — one as a Long, another as an ISO string. The `@Contextual` annotation on the data class stays unchanged; the behavior is chosen by which module is active at encode time. ## Decode symmetry The same module must be present when decoding, or `decodeFromString` throws because the ContextualSerializer can't find the binding. Encoding and decoding must agree on the module and the wire format the descriptor implies.

  • How would you make the same Date emit an ISO-8601 string instead?
    Keep @Contextual on the class, but register a different serializer whose descriptor is PrimitiveKind.STRING and whose serialize/deserialize convert Date <-> ISO string. Attach that module to a different Json instance.
  • What does the contextual provider lambda (args -> serializer) solve?
    Parameterized contextual types: it receives the resolved serializers for the type arguments so you can build a serializer for, say, Box<Foo> at runtime.

saying these in an interview costs you the question

  • Omits the descriptor or gives it a non-unique/clashing serial name
  • Forgets to attach the module to the Json instance
  • Registers for encode but not decode and is surprised decoding fails
  • Thinks contextual() needs the KClass passed explicitly always (the inferring overload exists)
  • Uses Date(value.time) on encode side, confusing serialize and deserialize

context