Handling JSON in Ktor without Manual String Parsing
Stop manually parsing JSON strings in Ktor. Learn how to use the ContentNegotiation plugin with kotlinx.serialization to automate data conversion between HTTP bodies and Kotlin objects.
05 Apr 2026, 15:04 UTC

The Friction of Manual Serialization
When building an API, the goal is to focus on business logic, not the tedious process of converting Kotlin objects into JSON strings and back again. Manually calling a JSON library inside every route creates repetitive boilerplate and makes your endpoints fragile; if you change a data field, you have to update every manual mapping call.
The solution is Ktor's ContentNegotiation plugin. This feature decouples your application logic from the data format. Instead of dealing with strings, you work with strongly typed Kotlin objects, and Ktor handles the conversion based on the HTTP Content-Type and Accept headers.
How ContentNegotiation Bridges the Gap
ContentNegotiation acts as a middleware. When a request arrives, the plugin looks at the Content-Type header to decide which converter to use to turn the request body into a Kotlin object via call.receive<T>(). Similarly, when you send a response using call.respond(obj), it checks the Accept header to determine if it should send JSON, XML, or another supported format.
While Ktor provides the plugin architecture, it doesn't perform the serialization itself. It relies on external libraries. The most common pairing is kotlinx.serialization, which is a compiler-plugin-based library that generates serialization logic at compile time rather than using slow reflection at runtime.
Implementing Automated JSON Conversion
To use this setup, you need both the Ktor plugin and the serialization library. Ensure your build.gradle.kts includes the kotlinx-serialization plugin and the ktor-server-content-negotiation and ktor-serialization-kotlinx-json dependencies.
Worked Example: A Simple User API
In this example, we define a data class for a User and configure the server to handle JSON automatically.
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
// 1. Mark data classes as @Serializable
@Serializable
data class User(val id: Int, val name: String, val email: String)
fun Application.module() {
// 2. Install and configure the plugin
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
routing {
post("/user") {
// 3. Automatically deserialize request body to User object
val user = call.receive<User>()
// Perform business logic here
call.respond(user) // 4. Automatically serialize object to JSON
}
}
}Execution and Verification
Run the server and use a tool like curl to test the endpoint. You must provide the Content-Type: application/json header, or Ktor will not know which converter to use.
# Run this in your terminal
curl -X POST http://localhost:8080/user \
-H "Content-Type: application/json" \
-d '{"id": 1, "name": "Jane Doe", "email": "[contact removed]"}'Expected Result: The server should return the same JSON object with a Content-Type: application/json response header.
Trade-offs and Common Pitfalls
While ContentNegotiation simplifies the workflow, there are specific constraints to keep in mind:
- The Compiler Requirement: Because
kotlinx.serializationuses a compiler plugin, simply adding the runtime library is not enough. If you forget the Gradle plugin, you will encounter errors stating that the class is not serializable. - Strict Header Matching: If a client sends a request with an unsupported or missing
Content-Typeheader, Ktor will throw anUnsupportedMediaTypeException. You may need to implement a status page plugin to catch this and return a clean 415 Unsupported Media Type response to the client. - Type Safety:
call.receive<T>()is type-safe, but it assumes the incoming JSON matches the structure of your@Serializableclass. If the JSON is malformed or missing required fields, it will throw aBadRequestException.
Final Checklist for Implementation
To ensure your JSON integration is production-ready, verify the following:
- All data transfer objects (DTOs) are annotated with
@Serializable. - The
ContentNegotiationplugin is installed before the routing block in your application module. - You have configured
ignoreUnknownKeys = truein the Json settings if your API needs to be resilient to additional fields sent by clients. - You have tested the endpoint with an incorrect
Content-Typeto verify how your application handles unsupported media types.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.