Compare @Contextual, @Serializable(with = ...), and @file:UseContextualSerialization. When does each resolve, and which one would you choose?
answer
- with = ... => compile-time, fixed, type-safe
- @Contextual => runtime module, variable, can fail at runtime
- @file:UseContextualSerialization => bulk @Contextual sugar
- No fallback between mechanisms
- Default to with=...; @Contextual only when flexibility is real
basics
~10 s@Serializable(with=...) hard-wires one serializer at compile time. @Contextual defers to a runtime module so it can vary. @file:UseContextualSerialization is just @Contextual applied to every use of a type in a file.
solid answer
~50 sAll three attach a custom serializer to a type you can't annotate, but they differ in binding time and scope. @Serializable(with = DateSerializer::class) on a property or type resolves at compile time to exactly that serializer — type-safe, no module needed, but fixed. @Contextual on a property defers resolution to a SerializersModule consulted at encode/decode time; the same property can serialize differently across formats, at the cost of a runtime SerializationException if unregistered. @file:UseContextualSerialization(Date::class) is sugar: it marks every occurrence of those types in the file as contextual, avoiding repetitive per-property @Contextual. Choose @Serializable(with=...) when one fixed strategy suffices (most cases); choose @Contextual when the strategy must vary by context/format or when a library wants consumers to plug in their own serializer; choose the file-level form to reduce annotation noise when many properties share a contextual type.
go deeper
Recognizes all three attach a custom serializer but may blur the binding-time distinction.
Clearly separates compile-time (with=) from runtime (@Contextual) and knows the file-level form is bulk sugar.
Gives a defensible decision guide, notes no fallback between mechanisms, and the library-pluggability use case.
Weighs runtime-failure risk vs flexibility for public API contracts and schema evolution; standardizes team conventions.
## The three mechanisms All address the same need — a custom serializer for a type — but bind differently. ### 1. @Serializable(with = ...) — compile-time, fixed ```kotlin @Serializable data class Event( @Serializable(with = DateAsLongSerializer::class) val ts: Date, ) ``` The plugin wires `DateAsLongSerializer` directly into the generated serializer. No module needed; resolution is compile-time and type-safe. If the serializer is missing/wrong, you find out at build time. The downside: it is **fixed** — every encode uses this exact serializer. You can also apply it at the type-alias or class level: ```kotlin typealias EpochDate = @Serializable(DateAsLongSerializer::class) Date ``` ### 2. @Contextual — runtime, variable ```kotlin @Serializable data class Event(@Contextual val ts: Date) ``` Resolution is deferred to the `SerializersModule` of the active format. Benefits: the same data class can serialize `Date` one way in JSON and another in CBOR; a library can expose `@Contextual` and let consumers register their own serializer. Cost: **no compile-time guarantee** — a missing `contextual(...)` registration throws `SerializationException` at runtime. ### 3. @file:UseContextualSerialization — bulk contextual ```kotlin @file:UseContextualSerialization(Date::class, Instant::class) package com.example ``` This is pure ergonomics: every property of those types in the file behaves as if annotated `@Contextual`, so you don't repeat `@Contextual` on dozens of fields. It still requires the serializer to be registered in a module at runtime — same trade-offs as `@Contextual`. ## Decision guide | Need | Use | |------|-----| | One fixed strategy, want compile-time safety | `@Serializable(with = ...)` | | Strategy varies by format/context, or library pluggability | `@Contextual` | | Many properties of the same un-annotatable type | `@file:UseContextualSerialization` | ## Subtle interaction If a property is `@Contextual` **and** the format's module has no registration, but a `@Serializable(with=...)` exists for the type elsewhere, the `@Contextual` slot still fails — it strictly consults the module. The mechanisms don't fall back to each other. Pick deliberately. ## Why not always @Contextual? Because you trade a compile error for a production runtime exception. For a single fixed format, `@Serializable(with=...)` is safer and needs no module wiring. `@Contextual` earns its keep only when the flexibility is real.
- A library author wants consumers to control how Date is serialized. Which mechanism and why?@Contextual (or @file:UseContextualSerialization), because resolution defers to the consumer's SerializersModule, letting each consumer plug in their own KSerializer<Date> without editing the library's data classes.
- Does @file:UseContextualSerialization remove the need for a registered serializer?No. It only marks usages as contextual; you still must register a serializer in the module or it fails at runtime exactly like @Contextual.
saying these in an interview costs you the question
- Claims @Contextual is always safer than @Serializable(with=...)
- Thinks @file:UseContextualSerialization auto-provides the serializer
- Expects @Contextual to fall back to a class-level @Serializable(with=...)
- Cannot articulate the compile-time vs runtime binding difference