Configuring Ktor HTTP Client for JSON with kotlinx.serialization and ContentNegotiation
Configure Ktor’s HTTP client to automatically encode and decode JSON using kotlinx.serialization and the ContentNegotiation plugin. Follow the step‑by‑step guide, add the right dependencies, install the plugin, and handle errors properly.
20 Aug 2026, 11:34 UTC

Goal: Automatic JSON Serialization in Ktor Client
When a Ktor client talks to a JSON API, you want the request body to be automatically encoded and the response body decoded into Kotlin objects. The ContentNegotiation plugin, paired with kotlinx.serialization, gives you that behavior with minimal boilerplate.
Prerequisites
- Project using Gradle (Kotlin DSL or Groovy) with Kotlin 1.9+.
- Ktor client library (any engine you prefer – CIO, Jetty, Apache, OkHttp).
- kotlinx.serialization‑json artifact for JSON support.
- Data classes that model the API payloads, annotated with
@Serializableor registered in a custom serializers module.
Dependency Selection
Add the following to your build.gradle.kts (replace 1.9.0 with the latest Ktor release you use):
implementation("io.ktor:ktor-client-core:2.3.0")
implementation("io.ktor:ktor-client-content-negotiation:2.3.0")
implementation("io.ktor:ktor-client-cio:2.3.0") // or another engine
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
Verify that the Gradle sync resolves all four artifacts. The ktor-client-content-negotiation artifact pulls in the kotlinx.serialization API but not the JSON implementation, so you must include kotlinx-serialization-json explicitly.
Installing the Plugin
Instantiate the client in a single place (e.g., a singleton or a DI container) and install the plugin inside the builder:
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
encodeDefaults = false
})
}
}
The json() call registers a Json instance that the plugin uses for both encoding and decoding. Adjust the Json configuration to match your API’s expectations (e.g., allow trailing commas, ignore unknown keys, etc.).
Defining Serializable Data Classes
Mark each Kotlin data class with @Serializable:
import kotlinx.serialization.Serializable
@Serializable
data class User(val id: Int, val name: String, val email: String)
For polymorphic types or custom serializers, register them in a module and pass that module to the Json builder. Example for a sealed class hierarchy:
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
val module = SerializersModule {
polymorphic(Animal::class) {
subclass(Dog::class)
subclass(Cat::class)
}
}
val json = Json {
serializersModule = module
ignoreUnknownKeys = true
}
Making a Request
With the client ready, you can send a POST with a serializable body and receive a deserialized response:
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
suspend fun createUser(user: User): User {
return try {
client.post("https://api.example.com/users") {
contentType(ContentType.Application.Json)
setBody(user)
}.body()
} catch (e: ClientRequestException) {
// 4xx responses – check status and body
val errorBody = e.response.bodyAsText()
throw RuntimeException("Bad request: $errorBody", e)
} catch (e: SerializationException) {
// JSON mapping failure
throw RuntimeException("Failed to parse response", e)
}
}
Key points:
contentType(ContentType.Application.Json)is optional – the plugin sets it automatically forsetBodywhen the body is serializable.- Use
.body()to read the response body as a specific type. If the JSON cannot be mapped, Ktor throws aSerializationException. - Wrap the call in
try/catchto handle HTTP errors and deserialization problems separately.
Testing the Configuration
- Run a simple coroutine that calls
createUserwith a validUserinstance. - Verify the server receives a JSON body that matches the
Userstructure. You can use a mock server or a request‑inspection endpoint. - Return a JSON payload from the server that matches the
Usertype and confirm the client returns aUserobject with the same data. - Change the server to return malformed JSON (e.g., missing quotes) and ensure the client throws a
SerializationExceptionthat you catch in your code.
Common Pitfalls and Recovery
- Missing dependency: If
kotlinx-serialization-jsonis omitted, the plugin compiles but throws a runtime exception when trying to encode/decode JSON. Check Gradle’s dependency graph. - Case‑sensitive keys: By default,
kotlinx.serializationis case‑sensitive. If your API uses snake_case, annotate fields with@SerialName("snake_case")or configureJson { namingStrategy = JsonNamingStrategy.SnakeCase }(available in newer releases). - Unknown fields: If the API adds new fields you don’t care about, set
ignoreUnknownKeys = truein theJsonconfiguration to avoid failures. - Duplicate converters: Do not add Jackson or Gson converters to the same client; the plugin order determines which converter handles a
Content-Typeheader. Stick to a single JSON library for consistency.
Engine‑Specific Notes
The JSON conversion logic is engine‑agnostic. However, each engine has its own TLS and HTTP/2 support. If you switch from CIO to OkHttp, for example, the client construction changes only in the engine parameter:
val client = HttpClient(OkHttp) { /* same plugin config */ }
All other code remains unchanged.
Conclusion
By adding the ktor-client-content-negotiation and kotlinx-serialization-json dependencies, installing the plugin with a Json configuration, and marking your data classes with @Serializable, you enable Ktor to automatically serialize request bodies and deserialize responses. Handle errors explicitly to surface problems early, and test against a controlled endpoint to confirm the round‑trip works as expected.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.