Why serializing collections of different element types is not supported in ktor-serialization?

1.9k views Asked by At

I am trying to make a simple server which gives serialized List in JSON. The List to be serialized is the example in the official blog post's Polymorphic serialization section.

But with the ktor's serialization feature, I get the following exception.

21:53:25.536 [nioEventLoopGroup-4-1] ERROR ktor.application - Unhandled: GET - /
java.lang.IllegalStateException: Serializing collections of different element types is not yet supported. Selected serializers: [DirectMessage, BroadcastMessage]
    at io.ktor.serialization.SerializerLookupKt.elementSerializer(SerializerLookup.kt:71)

Since sealed class is a key feature to choose Kotlin, I really wonder why this is not supported.

Are there any good reasons for ktor-serialization not supporting this? Or should I post an issue for removing this check from SerializerLookup.kt?


I made this code by choosing New Project > Kotlin > Application in IntelliJ. The modified code is shown below.

My server.kt:

import io.ktor.application.*
import io.ktor.features.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.serialization.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import kotlinx.serialization.Serializable

@Serializable
sealed class Message {
    abstract val content: String
}

@Serializable
data class BroadcastMessage(override val content: String) : Message()

@Serializable
data class DirectMessage(override val content: String, val recipient: String) : Message()

val data: List<Message> = listOf(
    DirectMessage("Hey, Joe!", "Joe"),
    BroadcastMessage("Hey, all!")
)

fun main() {
    embeddedServer(Netty, port = 8080, host = "127.0.0.1") {
        install(ContentNegotiation) {
            json()
        }
        routing {
            get("/") {
                call.respond(data)
            }
        }
    }.start(wait = true)
}

My build.gradle.kts:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.4.10"
    application
    kotlin("plugin.serialization") version "1.4.10"
}
group = "com.example.ktor.serialization"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
    jcenter()
    maven {
        url = uri("https://dl.bintray.com/kotlin/ktor")
    }
    maven {
        url = uri("https://dl.bintray.com/kotlin/kotlinx")
    }
}
dependencies {
    testImplementation(kotlin("test-junit5"))
    implementation("io.ktor:ktor-server-netty:1.4.1")
    implementation("io.ktor:ktor-html-builder:1.4.1")
    implementation("io.ktor:ktor-serialization:1.4.1")
    implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.2")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.0")
    implementation("ch.qos.logback:logback-classic:1.2.3")
}
tasks.withType<KotlinCompile>() {
    kotlinOptions.jvmTarget = "11"
}
application {
    mainClassName = "ServerKt"
}
2

There are 2 answers

0
romainbsl On

As said in the stacktrace this is not supported yet., so it might come someday.

However, a workaround is still possible for such a case. The issue is from Ktor, not Kotlinx Serialization. So, you can serialize your data as JSON, and then send them as a response, like here:

fun Application.module(testing: Boolean = false) {
    install(ContentNegotiation) { json() }

    routing {
        get("/") {
            val data: List<Message> = listOf(
                DirectMessage("Hey, Joe!", "Joe"),
                BroadcastMessage("Hey, all!")
            )

            val string = Json.encodeToString(data)
            call.respondText(string, contentType = ContentType.Application.Json)
        }
    }
}

@Serializable
sealed class Message {
    abstract val content: String
}

@Serializable
data class BroadcastMessage(override val content: String) : Message()

@Serializable
data class DirectMessage(override val content: String, val recipient: String) : Message()
0
Sergey Mashkov On

The reason is that we don't have the particular type information and can only analyze instance classes in runtime. Analyzing and runtime type intersection is not an easy task and for sure it will be very inefficient that is unacceptable on server-side.

Using typeOf could potentially help, but we haven't analyzed the performance impact of such a change (including allocation profile). The other reason is that we didn't know about typeOf (it didn't exist) and call.respond has been designed without it so this change will for sure be a breaking change.