Zero‑Boilerplate JSON in Ktor with kotlinx.serialization
Learn how Ktor’s ContentNegotiation plugin, paired with kotlinx.serialization, automatically handles JSON request and response bodies so you can focus on business logic.
08 Sept 2025, 03:38 UTC

When building a REST API in Kotlin, manually parsing JSON strings into data classes adds repetitive code and increases the chance of mismatched fields.
How ContentNegotiation picks a serializer
The ContentNegotiation plugin inspects the Content-Type header on incoming requests to choose a deserializer and the Accept header on outgoing responses to choose a serializer. If multiple codecs claim the same media type, the first registered codec wins unless you set an explicit priority.
Adding kotlinx.serialization to Ktor
First apply the Kotlin serialization plugin and add the Ktor content‑negotiation and serialization dependencies.
// build.gradle.kts
plugins {
kotlin("plugin.serialization") version "1.9.0"
}
dependencies {
implementation("io.ktor:ktor-server-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
}
Then install the plugin in your Application module and configure the JSON codec.
import io.ktor.server.plugins.contentnegotiation
import io.ktor.serialization.kotlinx.json
import kotlinx.serialization.json.Json
fun Application.module() {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
ignoreUnknownKeys = true
encodeDefaults = true
})
}
}
Defining a route that works without manual marshaling
With the plugin in place, a data class annotated with @Serializable can be returned or received directly.
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
@Serializable
data class User(val id: Int, val name: String, val email: String)
fun Route.userRoutes() {
get("/user/{id}") {
val user = User(42, "Ada Lovelace", "ada@example.com")
call.respond(user) // automatic JSON serialization
}
post("/user") {
val user = call.receive() // automatic JSON deserialization
call.respond(io.ktor.http.HttpStatusCode.Created, user)
}
}
Trade‑offs and things to watch
- Registering more than one JSON codec (e.g., Jackson and kotlinx.serialization) without setting priorities can lead to ambiguous resolution; the first codec in the list will be used for
Accept: application/json. - The
kotlinx.serializationplugin requires the Kotlin compiler plugin; omitting it yields a compile‑time error, not a runtime surprise. - Only classes marked with
@Serializable(or those with a registered serializer) can be processed; trying to route a third‑party class without a serializer results in a serialization exception.
Quick verification steps
- Start the application and send a GET request to
/user/42with anAccept: application/jsonheader; you should receive a JSON body matching theUserinstance. - POST a JSON payload to
/userwith aContent-Type: application/jsonheader; the server should deserialize it into aUserobject and return it with a 201 status. - At runtime you can inspect the registered codecs via
contentNegotiation.registrationsto verify that the JSON codec is present and prioritized as expected.
By letting Ktor’s ContentNegotiation handle the marshaling step, you remove boilerplate, keep your route functions focused on domain logic, and gain compile‑time safety through kotlinx.serialization’s generated code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.