Ktor client Content Negotiation plugin that provides automatic serialization and deserialization of request and response body content using various serialization formats like JSON, XML, and ProtoBuf
npx @tessl/cli install tessl/maven-io-ktor--ktor-client-content-negotiation-jvm@3.2.0Ktor client Content Negotiation plugin that provides automatic serialization and deserialization of request and response body content using various serialization formats like JSON, XML, and ProtoBuf. It handles content type negotiation by automatically setting Accept headers based on configured converters, processes incoming response content based on Content-Type headers, and provides extensible architecture for custom content converters.
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.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
val client = HttpClient {
install(ContentNegotiation) {
json() // Install JSON converter
}
}
// Automatic serialization on request
val response = client.post("https://api.example.com/users") {
setBody(User(name = "Alice", email = "alice@example.com"))
}
// Automatic deserialization on response
val user: User = client.get("https://api.example.com/users/1").body()The ContentNegotiation plugin is built around several key components:
ContentNegotiationConfig class for registering converters and configuring behaviorContentConverter interface implementations for specific serialization formatsContentTypeMatcher interface for flexible content type matchingInstall and configure the ContentNegotiation plugin with custom converters and settings.
val ContentNegotiation: ClientPlugin<ContentNegotiationConfig>
@KtorDsl
class ContentNegotiationConfig : Configuration {
var defaultAcceptHeaderQValue: Double?
override fun <T : ContentConverter> register(
contentType: ContentType,
converter: T,
configuration: T.() -> Unit
)
fun <T : ContentConverter> register(
contentTypeToSend: ContentType,
converter: T,
contentTypeMatcher: ContentTypeMatcher,
configuration: T.() -> Unit
)
inline fun <reified T> ignoreType()
fun ignoreType(type: KClass<*>)
inline fun <reified T> removeIgnoredType()
fun removeIgnoredType(type: KClass<*>)
fun clearIgnoredTypes()
}Usage Examples:
val client = HttpClient {
install(ContentNegotiation) {
// Register JSON converter
json()
// Register custom converter
register(ContentType.Application.Xml, XmlConverter()) {
// Configure converter
}
// Set Accept header quality value
defaultAcceptHeaderQValue = 0.8
// Ignore specific types
ignoreType<String>()
removeIgnoredType<ByteArray>()
}
}Flexible content type matching for JSON and custom formats.
/**
* Matcher that accepts all extended json content types
*/
object JsonContentTypeMatcher : ContentTypeMatcher {
override fun contains(contentType: ContentType): Boolean {
if (contentType.match(ContentType.Application.Json)) {
return true
}
val value = contentType.withoutParameters().toString()
return value in ContentType.Application && value.endsWith("+json", ignoreCase = true)
}
}
/**
* Interface for any objects that can match a ContentType.
*/
interface ContentTypeMatcher {
/**
* Checks if this type matches a contentType type.
*/
fun contains(contentType: ContentType): Boolean
}Usage Examples:
// Use built-in JSON matcher for extended JSON types
install(ContentNegotiation) {
register(
contentTypeToSend = ContentType.Application.Json,
converter = JsonConverter(),
contentTypeMatcher = JsonContentTypeMatcher
)
}
// Create custom matcher
val customMatcher = object : ContentTypeMatcher {
override fun contains(contentType: ContentType): Boolean {
return contentType.match(ContentType.Application.Any) &&
contentType.toString().endsWith("+custom")
}
}Core interfaces for implementing custom content converters.
/**
* 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 is a JSON content converter that provides both
* serialization and deserialization.
*/
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.
*
* @param charset response charset
* @param typeInfo response body typeInfo
* @param contentType to which this data converter has been registered
* @param value to be converted
* @return a converted [OutgoingContent] value, or null if [value] isn't suitable
*/
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 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 = {}
)
}Usage Examples:
class CustomConverter : ContentConverter {
override suspend fun serialize(
contentType: ContentType,
charset: Charset,
typeInfo: TypeInfo,
value: Any?
): OutgoingContent? {
return when (value) {
is MyCustomType -> TextContent(
value.serialize(),
contentType.withCharset(charset)
)
else -> null
}
}
override suspend fun deserialize(
charset: Charset,
typeInfo: TypeInfo,
content: ByteReadChannel
): Any? {
if (typeInfo.type == MyCustomType::class) {
val text = content.readUTF8Line()
return MyCustomType.deserialize(text)
}
return null
}
}Exclude specific content types from Accept headers on a per-request basis.
fun HttpRequestBuilder.exclude(vararg contentType: ContentType)Usage Examples:
// Exclude JSON from a specific request
val response = client.get("https://api.example.com/data") {
exclude(ContentType.Application.Json)
}
// Exclude multiple content types
client.post("https://api.example.com/upload") {
exclude(ContentType.Application.Json, ContentType.Application.Xml)
setBody(rawData)
}Handle content conversion errors with detailed exception information.
/**
* Base exception for content conversion errors
*/
open class ContentConvertException(
message: String,
cause: Throwable? = null
) : Exception(message, cause)
/**
* JSON-specific conversion exception
*/
class JsonConvertException(
message: String,
cause: Throwable? = null
) : ContentConvertException(message, cause)
/**
* Exception for content conversion failures specific to ContentNegotiation plugin
*/
class ContentConverterException(message: String) : Exception(message)Usage Examples:
try {
val result: MyType = client.get("https://api.example.com/data").body()
} catch (e: JsonConvertException) {
println("JSON conversion failed: ${e.message}")
// Handle JSON-specific error
} catch (e: ContentConvertException) {
println("Content conversion failed: ${e.message}")
// Handle general content conversion error
} catch (e: ContentConverterException) {
println("ContentNegotiation plugin error: ${e.message}")
// Handle plugin-specific error
}Helper functions for charset detection and content processing.
/**
* 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?Usage Examples:
// In a custom converter
override suspend fun deserialize(
charset: Charset,
typeInfo: TypeInfo,
content: ByteReadChannel
): Any? {
val headers = /* get headers from context */
val detectedCharset = headers.suitableCharset(Charsets.UTF_8)
// Use detected charset for processing
}// Core plugin type
val ContentNegotiation: ClientPlugin<ContentNegotiationConfig>
// Configuration class
@KtorDsl
class ContentNegotiationConfig : Configuration {
var defaultAcceptHeaderQValue: Double?
}
// JSON content type matcher
object JsonContentTypeMatcher : ContentTypeMatcher
// Exception classes for conversion failures
open class ContentConvertException(message: String, cause: Throwable? = null) : Exception
class JsonConvertException(message: String, cause: Throwable? = null) : ContentConvertException
class ContentConverterException(message: String) : Exception
// Extension function for request exclusion
fun HttpRequestBuilder.exclude(vararg contentType: ContentType)
// Utility functions for charset handling
fun Headers.suitableCharset(defaultCharset: Charset = Charsets.UTF_8): Charset
fun Headers.suitableCharsetOrNull(defaultCharset: Charset = Charsets.UTF_8): Charset?By default, the following types are ignored and bypass content negotiation:
ByteArray::classString::classHttpStatusCode::classByteReadChannel::classOutgoingContent::classInputStream::classThe plugin integrates with various Ktor serialization libraries:
// JSON with kotlinx.serialization
install(ContentNegotiation) {
json()
}
// JSON with custom configuration
install(ContentNegotiation) {
json(Json {
prettyPrint = true
ignoreUnknownKeys = true
})
}
// XML support
install(ContentNegotiation) {
xml()
}
// Multiple formats
install(ContentNegotiation) {
json()
xml()
cbor()
}