Adding contextual serialization in Ktor

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: