Ktor ContentNegotiation with Kotlinx Serialization: Install, Configure, and Avoid Common Pitfalls
Learn how to set up Ktor’s ContentNegotiation plugin with Kotlinx Serialization, see a full example, and avoid common mistakes like missing dependencies or unsupported media types. Understand the plugin’s limits and how to test it properly.
04 Sept 2026, 23:16 UTC

Why ContentNegotiation Is the First Step for JSON APIs
When you expose a REST endpoint in Ktor, you almost always want to accept JSON from the client and return JSON back. The ContentNegotiation plugin bridges the gap between raw HTTP bodies and Kotlin data classes using Kotlinx Serialization. It automatically looks at the Content-Type header, picks the right converter, and serializes/deserializes the request and response.
Getting Started: Dependency and Plugin Installation
First add the serialization dependency to your module:
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.0")
Then register the plugin before any routes that need JSON:
import io.ktor.server.application.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
fun Application.module() {
install(ContentNegotiation) {
json(Json {
encodeDefaults = true
ignoreUnknownKeys = true
})
}
// Routes go after the plugin
routing {
post("/users") {
val user = call.receive()
call.respond(HttpStatusCode.Created, user)
}
}
}
Replace User with your own @Serializable data class. The json() block configures the underlying Json instance; you can tweak pretty printing, default values, or unknown key handling.
How It Works Under the Hood
- When a request hits a
postroute, Ktor reads theContent-Typeheader. - If it matches a registered converter (e.g.,
application/json), the plugin callscall.receive<User>()which deserializes the body. - When you call
call.respond(User), the plugin serializes the object back to JSON and setsContent-Type: application/jsonon the response. - If the header is missing or unsupported, Ktor automatically throws a
415 Unsupported Media Type. - Deserialization errors (malformed JSON, missing required fields) result in a
400 Bad Request.
Common Mistakes and How to Avoid Them
- Skipping the dependency: The plugin won’t compile without
ktor-serialization-kotlinx-json. Verify yourbuild.gradle.ktsincludes it. - Installing after routes: The plugin must be installed before any route that uses
call.receiveorcall.respondwith a serializable type. - Wrong content type: Clients must send
Content-Type: application/json. If they sendtext/plain, you’ll get a 415. Test with a tool likecurl -H "Content-Type: application/json" -d '{"name":"Alice"}' http://localhost:8080/users. - Ignoring exceptions:
call.receivethrowsContentTransformationExceptionon bad input. If you want custom error messages, wrap it in a try/catch and return a friendly JSON response. - Using multipart/form-data with JSON converter: The plugin won’t handle file uploads. For multipart, use
call.receiveMultipart()instead. - Assuming validation: The plugin only serializes/deserializes. If you need to enforce business rules, add explicit validation logic or a library such as
kotlinx.validation. - Client/server mismatch: If you build a Ktor HTTP client that doesn’t install the same converter, the client may send
application/jsonbut the server will reject it with a 415. Ensure both sides share the same serialization configuration.
Limitations to Keep in Mind
| Feature | Supported? | Notes |
|---|---|---|
| JSON (application/json) | ✅ | Default converter; add install(ContentNegotiation) { json() } |
| CBOR, Smile, Protobuf | ✅ (with extra dependencies) | Add ktor-serialization-kotlinx-cbor, ktor-serialization-kotlinx-protobuf, etc. |
| Multipart/form-data | ❌ | Use call.receiveMultipart() instead. |
| Binary streams | ❌ | Handle manually with call.receiveChannel(). |
| Schema generation/validation | ❌ | Integrate external tools if needed. |
Practical Verification Checklist
- Run the server and POST a valid JSON body; expect
201 Createdwith the same payload. - Send a request with
Content-Type: text/plain; expect415 Unsupported Media Type. - Remove the
json()configuration; POST with JSON again; expect400 Bad Requestdue to missing converter. - Test a malformed JSON body; verify you receive a
400 Bad Requestand the error message mentions deserialization failure.
When to Use Alternative Approaches
If you need advanced validation, schema enforcement, or support for XML, consider:
ktor-serialization-jacksonfor Jackson-based JSON with schema support.- Custom
ApplicationFeaturethat wraps serialization and validation logic. - Using
kotlinx.serializationwithkotlinx.validationorKtor Validationplugin.
Conclusion
The ContentNegotiation plugin with Kotlinx Serialization is a lightweight, coroutine-friendly solution for most JSON APIs in Ktor. Install the dependency, register the plugin early, and remember the limits—especially the lack of multipart handling and validation. With these practices, you’ll avoid the most common pitfalls and deliver a reliable, type-safe endpoint.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.