Show how to write a KSerializer for java.util.Date and register it so a @Contextual property serializes. What exactly does contextual() bind?
answer
- KSerializer = descriptor + serialize + deserialize
- Date.time is epoch millis (Long)
- contextual(serializer) infers KClass from generic
- Module attaches to the format instance (Json { serializersModule })
- Provider lambda overload handles generic types
basics
~10 sWrite 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 sYou 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 linesobject 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
Can copy the three KSerializer methods and call contextual(serializer) without fully explaining descriptor mechanics.
Writes the serializer correctly, knows contextual() infers the KClass, and attaches the module to the format.
Explains descriptor naming pitfalls, the provider-lambda overload for generics, and encode/decode module symmetry.
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