Simplify JSON Responses in Ktor with ContentNegotiation
Learn how Ktor’s ContentNegotiation plugin removes manual JSON boilerplate by automatically serializing Kotlin data classes to JSON.
01 Mar 2026, 01:38 UTC

Problem: repetitive JSON boilerplate in routes
When building a Ktor server, each endpoint often needs to turn a Kotlin data class into a JSON string manually, for example:
get("/users") {
call.respondText(gson.toJson(User(1, "Alice")), ContentType.Application.Json)
}
Repeating this pattern across many routes adds noise, makes it easy to forget a content‑type header, and couples the handler to a specific serializer.
Thesis: let ContentNegotiation handle the conversion
Ktor’s ContentNegotiation plugin can automatically serialize return values when you install it with a JSON serializer (Kotlinx.serialization or Jackson). Once configured, a route can simply return the data class and the framework will produce the correct JSON payload.
Setup
- Add the ContentNegotiation artifact and a JSON serializer to your
build.gradle.kts(or Maven pom). For Kotlinx.serialization:
dependencies {
implementation("io.ktor:ktor-server-content-negotiation:2.3.0")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.0")
}
You need read/write access to the project directory to edit the build file and reload the project.
- In the
Applicationmodule, install the plugin and configure the JSON serializer:
fun Application.main() {
install(ContentNegotiation) {
json() // uses Kotlinx.serialization by default
}
// …routing etc.
}
No extra permissions are required beyond those needed to run the application.
Worked example: a GET endpoint returning a data class
Define a simple data class with Kotlinx.serialization annotations:
@Serializable
data class User(val id: Int, val name: String)
Create a route that returns an instance directly:
get("/users") {
call.respond(User(1, "Alice"))
}
When a client calls GET /users, Ktor will:
- Detect that the response body is a
Userinstance. - Lookup the registered JSON serializer.
- Serialize the instance to
{"id":1,"name":"Alice"}and setContent-Type: application/json.
You can verify the behavior with a command such as (run from a terminal with network access to the running server):
curl -i http://localhost:8080/users
Expected checks:
- Response status code is 200.
- Header
Content-Typeincludesapplication/json. - Body matches the JSON representation of the returned data class.
No manual serialize() or convertToJson() calls appear in the handler; adding a new property to User automatically appears in the output without touching the route.
Trade‑off and limitation
The plugin relies on reflection or runtime metadata supplied by the serializer. This introduces a small overhead compared to hand‑crafted serialization, which may become noticeable under very high request rates (e.g., tens of thousands of requests per second). If you need ultra‑low latency, benchmark both approaches.
Version compatibility is another consideration: the Ktor version, the ktor-serialization-kotlinx-json artifact, and the Kotlinx.serialization library must be aligned. Mismatched versions can lead to NoSuchMethodError or silent failures where the fallback is to treat the object as a string. Always consult the Ktor release notes for the recommended triplet of versions.
A practical way to confirm that ContentNegotiation is active is to add a logging interceptor:
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.INFO
}
When a request hits a route, you should see a log line indicating that the response was processed by the JSON serializer.
Actionable closing
If you are building a Ktor API and find yourself repeatedly writing JSON conversion code, enable ContentNegotiation once at the application level and let your routes return Kotlin data classes directly. The reduction in boilerplate and the gain in type‑safety usually outweigh the modest serialization cost, especially for typical web‑service workloads.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.