Choosing the Right Serializer in Ktor: Managing Content Negotiation for REST APIs
Learn how to configure Ktor’s ContentNegotiation to serve JSON, XML, or custom MIME types based on the client’s Accept header. Follow a step‑by‑step example, trade‑offs, and a practical checklist to avoid common pitfalls.
10 Jun 2026, 23:55 UTC

Problem
When building a REST API with Ktor you often want the server to return data in the format the client asks for – JSON, XML, CSV, whatever. The client signals its preference with the Accept header. If Ktor’s content negotiation isn’t wired up correctly, the API will always return the same MIME type (usually application/json), or worse, it will return the wrong format and the client will fail to parse it. The challenge is to set up the negotiation pipeline so that the right serializer is chosen for each request without adding unnecessary dependencies or breaking existing clients.
Content Negotiation in Ktor
Ktor’s ContentNegotiation plugin inspects the Accept header and matches it against the serializers that have been registered. The plugin is order‑sensitive – the first matching serializer wins. If no match is found, Ktor falls back to the first registered serializer, which is often JSON.
Supported serializers in Ktor 2.x include:
- Kotlinx.serialization – lightweight, compile‑time plugin required.
- Jackson – full‑fledged Java library, needs Java 8+.
- Gson – Google’s JSON library, pure Kotlin/Java.
- XML (kotlinx-serialization‑xml) – optional XML support.
Selecting a Serializer
Decide based on:
- Runtime footprint – Kotlinx is the smallest.
- Feature set – Jackson offers advanced annotations and data‑binding.
- Language compatibility – Kotlinx works seamlessly with Kotlin data classes.
- Dependency conflicts – Jackson pulls in many transitive libs; avoid if you’re tight on size.
When you register serializers, put the most common one first. For example, if most clients expect JSON, register json() before xml() so that a request with Accept: application/xml will still match the XML serializer, but a request with no Accept header will default to JSON.
Custom Negotiators
Sometimes you need a MIME type that Ktor doesn’t support out of the box, such as application/vnd.myapp.v1+json. Register a custom negotiator with register:
install(ContentNegotiation) {
register("application/vnd.myapp.v1+json") { request, contentType, value ->
// Use kotlinx.serialization or Jackson under the hood
json().serialize(request, contentType, value)
}
}
Remember that custom negotiators are evaluated in registration order, so place them before generic ones if you want them to win.
Worked Example: JSON vs XML
- Define a data class:
- Set up the module:
- Test with curl:
- Verify MIME type:
@kotlinx.serialization.Serializable
data class User(val id: Int, val name: String)
fun Application.module() {
install(ContentNegotiation) {
json() // registers application/json
xml() // registers application/xml
}
routing {
get("/user") {
call.respond(User(1, "Alice"))
}
}
}
curl -H "Accept: application/json" http://localhost:8080/user
# => {"id":1,"name":"Alice"}
curl -H "Accept: application/xml" http://localhost:8080/user
# => <User id="1" name="Alice"/>
curl -i -H "Accept: application/xml" http://localhost:8080/user
# Check that the Content-Type header is application/xml
Trade‑offs and Limitations
- Order sensitivity – A mis‑ordered registration can silently override expected formats.
- Dependency bloat – Jackson pulls many transitive jars; use it only if you need its features.
- Runtime type information – Kotlinx requires the
kotlinx.serializationcompiler plugin; missing it will cause a runtime failure. - Non‑standard MIME types – Custom negotiators add complexity; maintain a clear registry and document it.
Practical Checklist
- Install
ContentNegotiationonce in the application module. - Register serializers in the order of frequency – most common first.
- Add custom MIME types before generic ones if they should take precedence.
- Run
curl -i -H "Accept: …" …for each supported type to confirm theContent‑Typeheader. - Verify that the application compiles with the required compiler plugin (e.g.,
kotlinx.serialization).
Closing
Proper content negotiation turns a brittle API into a flexible, client‑friendly interface. By carefully ordering serializer registrations, avoiding unnecessary dependencies, and testing each Accept header scenario, you can ensure that your Ktor service delivers the right format every time. Start with the minimal setup – just JSON – then add XML or custom MIME types as your clients evolve.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.