In Ktor, why do call.receive<User>() and call.respond(user) fail unless ContentNegotiation is installed?
answer
- Ktor pipelines only know bytes and text
- typed bodies need a registered converter
- Content-Type selects in, Accept selects out
- install(ContentNegotiation) { json() }
basics
~20 sKtor's pipelines natively handle only bytes, text and form data. ContentNegotiation registers converters that map a media type to a class, so without it no converter exists for User and both the receive and the respond transformation fail.
solid answer
~40 sKtor moves a request body and a response body through transformation pipelines. Out of the box those pipelines know `String`, `ByteArray`, `ByteReadChannel` and form parameters — nothing turns arbitrary JSON into a `User`. `install(ContentNegotiation) { json() }` registers a converter against a media type, and from then on `call.receive<User>()` picks the converter that matches the request's `Content-Type` header and `call.respond(user)` picks the one matching the request's `Accept` header, writing the response `Content-Type` accordingly. Without the plugin, `call.receiveText()` and `call.respondText()` still work — you are back to hand-rolling serialization — while `receive<User>()` throws a content-transformation error and `respond(user)` fails in the response pipeline, surfacing as a 500 unless you map it.
go deeper
Be able to write the install block from memory and name what it enables: typed call.receive<T>() and call.respond(obj). Know that JSON support in Ktor is opt-in, not a default.
Explain the mechanics: converters are registered per media type, the request Content-Type selects one for receive and the Accept header selects one for respond. Name the artifacts you must add.
Show that you know the failure modes in production — a missing Content-Type from a caller, an unmapped transformation exception surfacing as 500, and multipart or form bodies bypassing the plugin entirely.
Own the decision of whether serialization lives in a plugin at the edge at all: one converter registry for the whole service versus per-route hand-serialization, and what that means for a shared API contract across teams.
## What the plugin actually is Ktor's server is a pipeline engine. A request body arrives as raw bytes on a `ByteReadChannel`, and a response is ultimately an `OutgoingContent` written back to the socket. Between your handler and those bytes sit two transformation pipelines: the *receive* pipeline, driven by `call.receive<T>()`, and the *response* pipeline, driven by `call.respond(value)`. Ktor ships with transformations for a handful of low-level types only — `String`, `ByteArray`, `ByteReadChannel`, `InputStream`, and form parameters via `call.receiveParameters()`. There is deliberately no built-in JSON support: serialization is a plugin decision, not a framework default. `ContentNegotiation` (package `io.ktor.server.plugins.contentnegotiation`) is that plugin. Installing it registers one or more `ContentConverter` instances, each bound to a `ContentType`: `install(ContentNegotiation) { json() }` registers the kotlinx.serialization converter for `application/json`. `jackson { }` and `gson { }` do the same with those libraries, and `register(contentType, converter)` is the generic form. ## What changes on the receive side With a converter installed, `call.receive<User>()` reads the request's `Content-Type` header, finds the converter registered for that media type, and asks it to deserialize the body into the requested type. The requested type is captured through Ktor's reified `TypeInfo`, so generic types such as `List<User>` survive erasure. If no converter matches the `Content-Type`, or the body cannot be turned into the target type, the call fails with a content-transformation error rather than returning null. A client that posts JSON but forgets the `Content-Type: application/json` header is the single most common cause of this in practice — the body is perfect, but nothing selects a converter. ## What changes on the respond side `call.respond(user)` does the mirror image: it looks at the request's `Accept` header, matches it against the registered content types, and serializes with the winning converter, setting the response `Content-Type` to the matched type. If exactly one converter is registered and the client sends no `Accept` header, that converter is used. Status plus body is the two-argument form, `call.respond(HttpStatusCode.Created, user)`. Without the plugin there is no transformation for a `User`, so the response pipeline cannot produce `OutgoingContent`; the exception escapes the handler and, with no `StatusPages` mapping, the client sees a 500. ## Wiring it up The plugin and the converter are separate artifacts. You need `io.ktor:ktor-server-content-negotiation` for the plugin plus, for kotlinx.serialization JSON, `io.ktor:ktor-serialization-kotlinx-json` and the `kotlin("plugin.serialization")` Gradle plugin. Data classes crossing the boundary are annotated `@Serializable`. Forgetting the compiler plugin produces a compile-time error on `@Serializable`, not a runtime surprise — which is one of the reasons teams pick kotlinx over reflection-based converters. ## What ContentNegotiation does not cover Three things routinely trip people up: - **Form submissions.** `application/x-www-form-urlencoded` is read with `call.receiveParameters()`, which does not go through a converter. - **Multipart uploads.** `multipart/form-data` is read with `call.receiveMultipart()` and iterated part by part; converters are not applied to it. - **The client.** Ktor's `HttpClient` has its own, separately named `ContentNegotiation` plugin in `io.ktor.client.plugins.contentnegotiation`. Installing the server one in a client (or importing the wrong package) is a classic mix-up, because the names are identical. ## The interview point The question is really testing whether you understand that Ktor is assembled rather than batteries-included. Nothing is JSON until you say so; typed `receive`/`respond` are a *capability you install*, and the media-type headers are what select among the converters you installed. A candidate who says "you just return the object and Ktor serializes it" has not run a Ktor app without the plugin.
- A client posts perfectly valid JSON and receive<User>() still fails. What do you check first?The request's `Content-Type` header. Ktor's ContentNegotiation selects a converter by media type, so a body sent as `text/plain` or with no `Content-Type` matches no converter and the transformation fails even though the bytes are valid JSON. Check the client's header before touching the serializer configuration.
- Can you register two converters, and how does Ktor choose between them?Yes — each `register` call binds a converter to a `ContentType`. On receive, the request's `Content-Type` picks one. On respond, Ktor matches the request's `Accept` header against the registered types and serializes with the winner, setting the response `Content-Type` to the matched media type. With one converter and no `Accept`, that converter is used.
- What still works if you never install ContentNegotiation?Everything untyped: `call.receiveText()`, `call.receiveChannel()`, `call.receiveParameters()` for form posts, `call.respondText()`, `call.respondBytes()` and `call.respond(HttpStatusCode.NoContent)`. You can even serialize by hand and call `respondText(json, ContentType.Application.Json)`. Only the typed object conversions are missing.
saying these in an interview costs you the question
- Thinking Ktor serializes data classes to JSON by default
- Confusing the server plugin with the client HttpClient plugin
- Believing ContentNegotiation also parses multipart uploads
- Assuming respond picks the type from the response, not Accept
- Forgetting the kotlinx serialization Gradle plugin and @Serializable