Compare the ways to wire a custom serializer: @Serializable(with=...), @file:UseSerializers, @Contextual + SerializersModule, and KSerializer<T> by ... delegation. When is each appropriate?
answer
- with= pins one site, compile-time
- @file:UseSerializers = whole file
- @Contextual + SerializersModule = runtime lookup, third-party
- missing contextual => runtime SerializationException
- by delegation reuses another serializer
basics
~10 sAnnotate one spot with @Serializable(with=...). Cover a whole file with @file:UseSerializers. Register globally (especially for third-party types) with @Contextual plus a SerializersModule. Use 'by' delegation to reuse another serializer's logic while tweaking the type.
solid answer
~40 sFour mechanisms: (1) `@Serializable(with = S::class)` on a class or property binds S at that exact use site — most explicit, compile-time. (2) `@file:UseSerializers(S::class)` applies S to every occurrence of its target type in that file — convenient but file-scoped and easy to miss. (3) `@Contextual` on a property tells the plugin 'look this up at runtime in the SerializersModule'; you register `contextual(MyType::class, S)` in a `SerializersModule` passed to `Json { serializersModule = ... }`. This is the way to inject serializers for third-party types app-wide without annotating them, and to swap implementations per-format. (4) `class S : KSerializer<T> by SomeOtherSerializer` (or via `delegate`) reuses an existing serializer's machinery. Precedence: an explicit `with=` on the property wins over file/contextual. Contextual requires the module to be wired or it throws at runtime.
code
kotlin · 11 linesval module = SerializersModule {
contextual(Instant::class, InstantSerializer)
}
val json = Json { serializersModule = module }
@Serializable
data class Event(
@Contextual val at: Instant, // runtime lookup
@Serializable(with = ColorSerializer::class) // pinned here
val color: Color,
)go deeper
Knows @Serializable(with=...) pins a serializer at one place.
Adds @file:UseSerializers for file scope and understands @Contextual exists.
Wires SerializersModule for third-party/per-format serializers, knows the runtime-failure mode and precedence rules.
Designs module composition for multi-format/multi-tenant setups and uses contextual to vary serializers per assembly without code changes.
## The four wiring mechanisms ### 1. `@Serializable(with = S::class)` — pin one site Applies the serializer to a single class or property. Most explicit and fully compile-time checked. ```kotlin @Serializable data class E(@Serializable(with = InstantSerializer::class) val at: Instant) // or on the type itself: @Serializable(with = ColorSerializer::class) class Color(...) ``` ### 2. `@file:UseSerializers(...)` — file-wide Registers serializers for *every* occurrence of their target types in the file, so you don't repeat `with=` on each property. ```kotlin @file:UseSerializers(InstantSerializer::class) ``` Downside: scope is invisible at the use site; a reader of one property may not realize a custom serializer applies. ### 3. `@Contextual` + `SerializersModule` — runtime lookup / third-party types Mark the property `@Contextual` and provide the serializer in a module: ```kotlin val module = SerializersModule { contextual(Instant::class, InstantSerializer) } val json = Json { serializersModule = module } @Serializable data class E(@Contextual val at: Instant) ``` Use when you can't annotate the type, want to choose the serializer at assembly time, or vary it per format/Json instance. If no contextual serializer is registered, (de)serialization throws `SerializationException` at runtime — it is *not* a compile error. ### 4. `KSerializer<T> by ...` — delegation Reuse another serializer's implementation: ```kotlin // delegate the whole interface to a generated/wrapping serializer class WrappedSerializer : KSerializer<Wrapped> by DelegateSerializer ``` More commonly you delegate the descriptor and call another serializer inside serialize/deserialize (the surrogate pattern). True `by` delegation is handy when T is a thin wrapper whose wire form equals another type's. ## Precedence & gotchas - An explicit `@Serializable(with=...)` at a property beats file-level and contextual resolution for that property. - `@file:UseSerializers` only affects the file it's declared in. - `@Contextual` without a registered serializer is a **runtime** failure — test it. - The same custom serializer works across formats; with `@Contextual` you can even register *different* serializers per `Json`/`ProtoBuf` instance. ## Choosing - One-off, want it obvious -> `with=`. - Many props of the same type in one file -> `@file:UseSerializers`. - Third-party type, app-wide, or per-format choice -> `@Contextual` + module. - Wire form identical to another serializable type -> `by` delegation.
- What happens if a @Contextual property has no registered serializer?It throws a SerializationException at (de)serialization time, not at compile time — so contextual wiring needs test coverage.
- Which wins: @Serializable(with=...) on a property or a contextual registration for that type?The explicit @Serializable(with=...) at the use site takes precedence over contextual/file-level resolution.
saying these in an interview costs you the question
- Thinking @Contextual works without a SerializersModule
- Assuming a missing contextual serializer fails at compile time
- Annotating a third-party type you can't modify (use @Contextual instead)
- Forgetting @file:UseSerializers is file-scoped only
- Believing you must pick one mechanism globally rather than per case