What are the ways to obtain a KSerializer instance in kotlinx.serialization, and how do Type.serializer() and the top-level serializer<T>() differ?
answer
- Type.serializer() = plugin-generated, fastest, compile-time
- Generic class: serializer takes child serializers (Box.serializer(User.serializer()))
- serializer<T>() reified uses typeOf<T>() for nested generics
- serializer(kType) for runtime KType
- ListSerializer / MapSerializer / .nullable combinators
basics
~10 sYou can call the generated MyType.serializer() function on a @Serializable class, or use the top-level serializer<T>() / serializer(KType) functions to look one up, including for generic types like List<User>.
solid answer
~40 sThree common paths: (1) `MyType.serializer()` — the function the compiler plugin generates for each @Serializable class; it is compile-time resolved and returns the exact KSerializer<MyType>. For generic classes the plugin generates an overload taking the type arguments' serializers, e.g. `Box.serializer(User.serializer())`. (2) The reified top-level `serializer<T>()` (in kotlinx.serialization), which uses `typeOf<T>()` to capture the full `KType`, then resolves a serializer — handling nested generics like `List<Map<String, User>>`. (3) `serializer(kType: KType)` for when you only have a runtime `KType`. There is also `PrimitiveSerializer`-style built-ins (`Int.serializer()`, `String.serializer()`) and `ListSerializer(elementSerializer)`. `Type.serializer()` is the most efficient (no reflection, no KType machinery); the top-level functions trade a little overhead for handling arbitrary generic types.
code
kotlin · 14 lines@Serializable data class User(val id: Int)
@Serializable data class Box<T>(val value: T)
// 1. generated
val a: KSerializer<User> = User.serializer()
// 1b. generic generated overload
val b: KSerializer<Box<User>> = Box.serializer(User.serializer())
// 2. reified top-level, handles nesting
val c: KSerializer<List<User>> = serializer()
// 3. from a runtime KType
val kType = typeOf<Map<String, User>>()
val d: KSerializer<Any?> = serializer(kType)
// 4. combinator
val e: KSerializer<List<User>> = ListSerializer(User.serializer())go deeper
Knows MyType.serializer() exists for @Serializable classes and that you pass it to Json.
Distinguishes generated vs reified vs KType-based lookup and knows generic classes need child serializers.
Explains how reified serializer<T>() uses typeOf<T>() to defeat erasure for nested generics and when frameworks use serializer(kType).
Reasons about performance (no-reflection generated path vs KType resolution) and library-design implications for framework integration points.
## The goal Before you can encode/decode, you need a `KSerializer<T>`. kotlinx.serialization offers several ways to get one, ranging from zero-overhead compile-time lookups to fully runtime-type-driven resolution. ## 1. Generated `Type.serializer()` For every `@Serializable` class the compiler **plugin generates** a function: ```kotlin @Serializable data class User(val id: Int, val name: String) val s: KSerializer<User> = User.serializer() // generated, compile-time ``` This is the cheapest path — no reflection, no `KType`. For **generic** serializable classes the plugin generates an overload that takes serializers for each type parameter: ```kotlin @Serializable data class Box<T>(val value: T) val s: KSerializer<Box<User>> = Box.serializer(User.serializer()) ``` You must supply the inner serializers yourself — the function can't infer them at runtime. ## 2. Top-level reified `serializer<T>()` The library provides: ```kotlin inline fun <reified T> serializer(): KSerializer<T> ``` It uses `reified` + `typeOf<T>()` to capture the **complete** `KType`, including nested generic arguments, then walks it to build a composite serializer: ```kotlin val s: KSerializer<List<Map<String, User>>> = serializer() ``` This is what makes `Json.encodeToString(value)` (the reified, serializer-less overload) work — it calls `serializer<T>()` for you. ## 3. `serializer(kType: KType)` When you only have a `KType` at runtime (e.g. obtained from reflection), call the overload that takes a `KType` and returns `KSerializer<Any?>`. Used by frameworks (e.g. Ktor content negotiation) that don't know `T` statically. ## 4. Built-ins and combinators - Primitives: `Int.serializer()`, `String.serializer()`, `Boolean.serializer()`, etc. - Collections: `ListSerializer(elem)`, `MapSerializer(k, v)`, `SetSerializer(elem)`. - Nullability: `elem.nullable` wraps a serializer to accept null. ## Choosing | Situation | Use | |---|---| | You know the concrete @Serializable type statically | `Type.serializer()` (fastest) | | Arbitrary nested generic, type known at compile time | reified `serializer<T>()` | | Only a runtime KType available | `serializer(kType)` | | Standalone primitive/collection | `Int.serializer()`, `ListSerializer(...)` | ## Gotcha `serializer<T>()` for a type that isn't @Serializable (and has no contextual/built-in serializer) throws `SerializationException` at runtime — the reified version can't be checked at compile time the way `Type.serializer()` can.
- Why does Box.serializer() require you to pass User.serializer() for Box<User>?Type erasure: the generated function for a generic class can't know the type argument at runtime, so you provide the element serializer explicitly. The reified serializer<T>() avoids this by capturing the KType.
- What happens if you call serializer<NotSerializable>()?It throws SerializationException at runtime because no serializer can be resolved; the reified call can't catch this at compile time.
saying these in an interview costs you the question
- Thinking Type.serializer() uses runtime reflection (it's generated, compile-time)
- Believing serializer<T>() loses nested generic type arguments (typeOf<T>() preserves them)
- Not knowing generic classes need child serializers passed in
- Confusing serializer(kType) (returns KSerializer<Any?>) with reified serializer<T>()