Ktor client Content Negotiation support - enables automatic serialization and deserialization of request and response bodies using various formats like JSON, XML, and others
npx @tessl/cli install tessl/maven-io-ktor--ktor-client-content-negotiation@3.2.0Ktor Client Content Negotiation provides automatic serialization and deserialization of request and response bodies using various formats like JSON, XML, and protobuf. It handles media type negotiation between client and server using Accept and Content-Type headers, while offering extensible converter registration mechanisms with built-in support for popular serialization libraries.
implementation("io.ktor:ktor-client-content-negotiation:3.2.0")import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.util.reflect.*
import io.ktor.utils.io.*
import io.ktor.utils.io.charsets.*import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.serialization.kotlinx.json.*
// Configure client with content negotiation
val client = HttpClient {
install(ContentNegotiation) {
json() // Uses kotlinx.serialization JSON
}
}
// Automatic serialization on send
data class User(val name: String, val email: String)
val user = User("Alice", "alice@example.com")
val response = client.post("https://api.example.com/users") {
setBody(user) // Automatically serialized to JSON
}
// Automatic deserialization on receive
val createdUser: User = response.body()Ktor Client Content Negotiation is built around several key components:
Install and configure the ContentNegotiation plugin with content converters and settings.
/**
* A plugin that serves two primary purposes:
* - Negotiating media types between the client and server using Accept and Content-Type headers
* - Serializing/deserializing content in specific formats when sending requests and receiving responses
*/
val ContentNegotiation: ClientPlugin<ContentNegotiationConfig>Register content converters for specific media types with optional configuration.
/**
* A ContentNegotiation configuration that is used during installation
*/
class ContentNegotiationConfig : Configuration {
/**
* By default, Accept headers for registered content types will have no q value (implicit 1.0).
* Set this to change that behavior for per-request basis Accept content type preferences.
*/
var defaultAcceptHeaderQValue: Double?
/**
* Registers a contentType to a specified converter with an optional configuration script
*/
fun <T : ContentConverter> register(
contentType: ContentType,
converter: T,
configuration: T.() -> Unit = {}
)
/**
* Registers a contentTypeToSend and contentTypeMatcher to a specified converter
* with an optional configuration script for advanced content type handling
*/
fun <T : ContentConverter> register(
contentTypeToSend: ContentType,
converter: T,
contentTypeMatcher: ContentTypeMatcher,
configuration: T.() -> Unit
)
}Usage Examples:
import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.serialization.kotlinx.xml.*
val client = HttpClient {
install(ContentNegotiation) {
json() // Register JSON converter
xml() // Register XML converter
// Custom q-value for Accept headers
defaultAcceptHeaderQValue = 0.8
// Register custom converter
register(
ContentType.Application.ProtoBuf,
MyProtobufConverter()
) {
// Configure the converter
customOption = true
}
}
}Configure which types should bypass content negotiation processing.
/**
* Adds a type to the list of types that should be ignored by ContentNegotiation.
* The list contains HttpStatusCode, ByteArray, String and streaming types by default.
*/
inline fun <reified T> ContentNegotiationConfig.ignoreType()
/**
* Adds a type to the list of types that should be ignored by ContentNegotiation
*/
fun ContentNegotiationConfig.ignoreType(type: KClass<*>)
/**
* Remove type from the list of types that should be ignored by ContentNegotiation
*/
inline fun <reified T> ContentNegotiationConfig.removeIgnoredType()
/**
* Remove type from the list of types that should be ignored by ContentNegotiation
*/
fun ContentNegotiationConfig.removeIgnoredType(type: KClass<*>)
/**
* Clear all configured ignored types including defaults
*/
fun ContentNegotiationConfig.clearIgnoredTypes()Usage Examples:
val client = HttpClient {
install(ContentNegotiation) {
json()
// Add custom types to ignore
ignoreType<MyStreamingType>()
ignoreType(CustomContent::class)
// Remove default ignored types if needed
removeIgnoredType<String>()
// Clear all and start fresh
clearIgnoredTypes()
}
}Exclude specific content types from Accept headers on a per-request basis.
/**
* Excludes the given ContentType from the list of types that will be sent in the Accept header
* by the ContentNegotiation plugin. Can be used to not accept specific types for particular requests.
* This can be called multiple times to exclude multiple content types.
*/
fun HttpRequestBuilder.exclude(vararg contentType: ContentType)Usage Examples:
import io.ktor.client.request.*
import io.ktor.http.*
// Exclude JSON from this specific request
val response = client.get("https://api.example.com/data") {
exclude(ContentType.Application.Json)
}
// Exclude multiple content types
val response2 = client.get("https://api.example.com/legacy") {
exclude(ContentType.Application.Json, ContentType.Application.Xml)
}Advanced JSON content type matching for extended JSON formats.
/**
* Matcher that accepts all extended json content types including application/json
* and types ending with +json suffix
*/
object JsonContentTypeMatcher : ContentTypeMatcher {
/**
* Checks if contentType matches JSON patterns (application/json or */*+json)
*/
override fun contains(contentType: ContentType): Boolean
}Handle content conversion failures and errors.
/**
* Exception thrown when content conversion fails
*/
class ContentConverterException(message: String) : Exception(message)
/**
* Exception thrown when no suitable converter found for deserialization
*/
class ContentConvertException(message: String) : Exception(message)Usage Examples:
import io.ktor.client.plugins.contentnegotiation.*
try {
val response = client.post("https://api.example.com/data") {
setBody(complexObject)
}
val result: MyDataClass = response.body()
} catch (e: ContentConverterException) {
println("Failed to convert content: ${e.message}")
// Handle conversion error
}/**
* A custom content converter that could be registered in ContentNegotiation plugin for any particular content type
* Could provide bi-directional conversion implementation.
* One of the most typical examples of content converter is a JSON content converter that provides both
* serialization and deserialization. Implementations must override at least one method.
*/
interface ContentConverter {
/**
* Serializes a value to the specified contentType to a OutgoingContent.
* This function could ignore value if it is not suitable for conversion and return null so in this case
* other registered converters could be tried or this function could be invoked with other content types
* it the converted has been registered multiple times with different content types.
*
* @param charset response charset
* @param typeInfo response body typeInfo
* @param contentType to which this data converter has been registered and that matches the client's Accept header
* @param value to be converted
* @return a converted OutgoingContent value, or null if value isn't suitable for this converter
*/
suspend fun serialize(
contentType: ContentType,
charset: Charset,
typeInfo: TypeInfo,
value: Any?
): OutgoingContent?
/**
* Deserializes content to the value of type typeInfo
* @return a converted value (deserialized) or null if the context's subject is not suitable for this converter
*/
suspend fun deserialize(charset: Charset, typeInfo: TypeInfo, content: ByteReadChannel): Any?
}
/**
* Configuration for client and server ContentNegotiation plugin
*/
interface Configuration {
fun <T : ContentConverter> register(
contentType: ContentType,
converter: T,
configuration: T.() -> Unit = {}
)
}
/**
* Interface for matching content types with custom logic
*/
interface ContentTypeMatcher {
/**
* Checks if this type matches a contentType type.
*/
fun contains(contentType: ContentType): Boolean
}/**
* Common ignored types across all platforms:
* ByteArray, String, HttpStatusCode, ByteReadChannel, OutgoingContent
*/
internal val DefaultCommonIgnoredTypes: Set<KClass<*>>
/**
* Platform-specific ignored types (expect/actual implementation):
* - JVM: includes InputStream
* - JS/WASM: empty set
* - Native: empty set
*/
internal expect val DefaultIgnoredTypes: Set<KClass<*>>/**
* Detect suitable charset for an application call by Accept header or fallback to defaultCharset
*/
fun Headers.suitableCharset(defaultCharset: Charset = Charsets.UTF_8): Charset
/**
* Detect suitable charset for an application call by Accept header or fallback to null
*/
fun Headers.suitableCharsetOrNull(defaultCharset: Charset = Charsets.UTF_8): Charset?The ContentNegotiation plugin can throw several types of exceptions:
Common error scenarios:
// Handle conversion errors gracefully
try {
val result: MyData = client.get("https://api.example.com/data").body()
} catch (e: ContentConverterException) {
// Log error and use fallback
logger.error("Content conversion failed", e)
// Handle error appropriately
}