What is Ktor?
Ktor is a Kotlin framework for building web applications and HTTP services.
This blog post will focus on an issue I ran into recently concerning to JSON serialization when using Ktor (adding contextual serialization).
If you're into Kotlin and you haven't tried Ktor yet, I highly suggest you try it out. You can read more about Ktor here.
JSON Serialization in Ktor
The process of turning objects into JSON data is a common task and most of the time it's a non-issue; turning an Int
into a JSON number is simple, turning a String
into a JSON string is simple and so on. Eventually you will run into a situation where you need to either customize your serialization logic or provide a serialization implementation for a non-primitive value.
The Ktor docs are great as a start and they even show you how to use three built-in converters (Gson
, Jackson
and kotlinx.serialization
). For my project I'm using kotlinx.serialization
and the issue at hand will be solved for that converter, but the concept is the same for the others.
A minimal Ktor
setup with JSON serialization might look like this:
@Serializable
data class Response(private val value: String)
fun main() {
embeddedServer(Netty, port = 8080, host = "127.0.0.1") {
install(ContentNegotiation) {
json()
}
routing {
get("/") {
call.respond(Response("Hello World!"))
}
}
}.start(wait = true)
}
As expected, visiting localhost:8080 returns the following:
{
"value": "Hello World!"
}
The Issue
Let's illustrate a case where you might run into an issue.
In my project, I want failing requests, those that generate server-side exceptions, to translate these exceptions into response objects with the properties errorMessage
and statusCode
.
One might start by attempting the following:
@Serializable
data class DuplicateEntryException(
private val errorMessage: String,
@Contextual // Required, this specifies that this converter is expected to be found during runtime
private val statusCode: HttpStatusCode = HttpStatusCode.Conflict
) : RuntimeException(errorMessage)
fun main() {
embeddedServer(Netty, port = 8080, host = "127.0.0.1") {
install(ContentNegotiation) {
json()
}
routing {
get("/") {
// Faked error for simplicity's sake
call.respond(DuplicateEntryException("This entry already exists"))
}
}
}.start(wait = true)
}
Starting the project now and visiting localhost:8080 will result in the following exception:
kotlinx.serialization.SerializationException: Serializer for class 'HttpStatusCode' is not found.
The Solution
As the exception is telling us, we need to register a serializer for the HttpStatusCode
type. There are two ways of doing this, depending on your use case. I will illustrate both variants below.
Our first order of business, which will be used in both solutions, is to define the custom serializer:
object HttpStatusCodeSerializer : KSerializer<HttpStatusCode> {
override fun deserialize(decoder: Decoder): HttpStatusCode =
HttpStatusCode.fromValue(decoder.decodeInt())
override fun serialize(encoder: Encoder, value: HttpStatusCode) {
encoder.encodeInt(value.value)
}
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("HttpStatusCode", PrimitiveKind.INT)
}
Basically, turn the HttpStatusCode
object into a primitive type, Int
in this case.
Next up we need to use this, the first way is to add it to our json()
converter definition, like so:
fun main() {
embeddedServer(Netty, port = 8080, host = "127.0.0.1") {
install(ContentNegotiation) {
json(Json {
serializersModule = SerializersModule {
contextual(HttpStatusCode::class) { HttpStatusCodeSerializer}
}
})
}
routing {
get("/") {
// Faked error for simplicity's sake
call.respond(DuplicateEntryException("This entry already exists"))
}
}
}.start(wait = true)
}
I prefer this solution when you want your serialization to be easily reusable, for example DuplicateEntryException
is generic enough to be reused across use cases in the project.
If you want to customize serialization in a more localized fashion, something that might only be used for some specific purpose, you can do the following instead:
@Serializable
data class DuplicateEntryException(
private val errorMessage: String,
@Contextual
@Serializable(with = HttpStatusCodeSerializer::class) // Inform which Serializer to use for this property
private val statusCode: HttpStatusCode = HttpStatusCode.Conflict
) : RuntimeException(errorMessage)
Resources
If you want to learn more about Ktor
or kotlinx.serialization
, you can read more at the following resources: