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

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"
}

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()

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.

Related

Kotlin serialization: nested polymorphic module

Recently I've been trying to implement Kotlinx Serialization for polymorphic class hierarchy. I used this guide, however my example was a bit more complex. I had the following class structure:
#Serializable
open class Project(val name: String)
#Serializable
#SerialName("owned")
open class OwnedProject(val owner: String) : Project("kotlin")
#Serializable
#SerialName("owned_owned")
class OwnedOwnedProject(val ownerOwner: String) : OwnedProject("kotlinx.coroutines")
And was trying to deserialize OwnedOwnedProject instance using following code:
val module = SerializersModule {
polymorphic(Project::class) {
polymorphic(OwnedProject::class) {
subclass(OwnedOwnedProject::class)
}
}
}
val format = Json { serializersModule = module }
fun main() {
val str = "{\"type\":\"owned_owned\",\"name\":\"kotlin\",\"owner\":\"kotlinx.coroutines\",\"ownerOwner\":\"kotlinx.coroutines.launch\"}"
val obj = format.decodeFromString(PolymorphicSerializer(Project::class), str)
println(obj)
}
However whatever combinations of SerializersModule definition I tried, it always ended up with kotlinx.serialization.json.internal.JsonDecodingException: Polymorphic serializer was not found for class discriminator 'owned_owned' error.
Could you please give me a hint: how to implement SerializersModule for given class structure (deeper than two)? Am I missing something?
It seems you would need to register OwnedOwnedProject directly under Project as well, so the serializer knows it's a possible subclass:
val module = SerializersModule {
polymorphic(Project::class) {
subclass(OwnedOwnedProject::class)
}
polymorphic(OwnedProject::class) {
subclass(OwnedOwnedProject::class)
}
}

Unresolved reference in Kotlin optic data class reference

Was playing a bit with arrow library for Kotlin and found this error right out of the documentation https://arrow-kt.io/docs/optics/ . What am I doing wrong?
Unresolved reference: company
the code is next, so it is not compiling due to an error in reference
package com.app
import arrow.optics.Optional
import arrow.optics.optics
#optics
data class Street(val number: Int, val name: String) {
companion object
}
#optics
data class Address(val city: String, val street: Street) {
companion object
}
#optics
data class Company(val name: String, val address: Address) {
companion object
}
#optics
data class Employee(val name: String, val company: Company) {
companion object
}
fun main() {
// an immutable value with very nested components
val john = Employee("John Doe", Company("Kategory", Address("Functional city", Street(42, "lambda street"))))
// an Optional points to one place in the value
val optional: Optional<Employee, String> = Employee.company.address.street.name
// and now you can modify into a new copy without nested 'copy's!
optional.modify(john, String::toUpperCase)
}
my dependencies are next
//region Arrow
implementation("io.arrow-kt:arrow-core:$arrow_version")
implementation("io.arrow-kt:arrow-fx-coroutines:$arrow_version")
implementation("io.arrow-kt:arrow-optics:$arrow_version")
//endregion
Your Gradle configuration seems to be missing some of the required Google KSP setup. You can find it in the Arrow Optics Setup section of the website, and below.
plugins {
id("com.google.devtools.ksp") version "$googleKspVersion"
}
dependencies {
ksp("io.arrow-kt:arrow-optics-ksp-plugin:$arrowVersion")
}
You also need to make IDEA aware of the generated sources, or it will not be able to correctly pick up the code for code highlighting and syntax reporting. The setup is explained on the official Kotlin Website, and shown below.
kotlin {
sourceSets.main {
kotlin.srcDir("build/generated/ksp/main/kotlin")
}
sourceSets.test {
kotlin.srcDir("build/generated/ksp/test/kotlin")
}
}
Add the code below (*.gradle.kts) in your build script.
It will register a new source dir so that your IDE will see generated extensions. This works regardless of the number of flavors and build types you have.
android {
// ...
androidComponents.onVariants { variant ->
val name = variant.name
sourceSets {
getByName(name).kotlin.srcDir("${buildDir.absolutePath}/generated/ksp/${name}/kotlin")
}
}
}

Serialize generic class using kotlinix.serialization in Kotlin/JS fails

Serializing a generic class with kotlinx.serialization succeeds in JVM but fails in JavaScript with message TypeError: tmp$.serializer is not a function. Please, see the following Unit Test.
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlin.test.Test
#Serializable
data class SerializationTest<T>(val myValue: T)
#Serializable
data class A(val st : SerializationTest<Int>)
class SerializationTests {
#Test
fun serializeWithKotlinx() {
// Succeeds in JVM but throws in JavaScript with "TypeError: tmp$.serializer is not a function"
Json.encodeToString(SerializationTest(3))
}
#Test
fun serializeWithKotlinxWithBox() {
// Succeeds always
Json.encodeToString(A(SerializationTest(3)))
}
}
How can I serialize a generic class in JavaScript?
See the docs here
Please note that this example works only on JVM because of serializer function restrictions. For JS and Native, explicit serializer should be used: format.encodeToString(PolymorphicSerializer(Project::class), data) You can keep track of this issue here.
In your case, this code:
Json.encodeToString(SerializationTest(3))`
uses generics, which is only available on JVM.
You'll have to manually pass the serializer to encodeToString(...)
#Test
fun serializeWithKotlinx() {
val encoded =
Json.encodeToString(
SerializationTest.serializer(Int.serializer()),
SerializationTest(3),
)
println(encoded)
}
Or use a SerializersModule (documented here):
#Test
fun serializeWithKotlinxSerializersModule() {
val module = SerializersModule {
contextual(SerializationTest::class) { args ->
SerializationTest.serializer(args[0])
}
}
val mapper = Json { serializersModule = module }
val encoded = mapper.encodeToString(
SerializationTest(3),
)
println(encoded)
}
As per this GitHub issue:
Unfortunately, this is a known problem and we do not support JS legacy anymore. Please use IR backend if possible
With JS IR it works well, giving the same results as JVM.

How to validate json in kotlin

There is a kotlin class with the following structure.
data class Person(
#field:Length(max = 5)
val name: String,
val phones: List<Phone>
)
data class Phone(
#field:Length(max = 10)
val number: String
)
When converting the json string through objectMapper, I want to receive all the violation values.
ex) JSON object is not valid. Reasons (3) name length must be 5, number length must be 10, ...
#Test
fun test() {
val json = """
{
"name": "name",
"phones": [
{ "number": "1234567890123456" },
{ "number": "1234567890123456" }
]
}
""".trimIndent()
try {
objectMapper.readValue(json, Person::class.java)
} catch (ex: ConstraintViolationException) {
val violations = ex.constraintViolations
println(violations.size) // expected size = 3
}
}
However, the above code fails to catch the exception and causes the exception below.
com.fasterxml.jackson.databind.JsonMappingException: JSON object is not valid. Reasons (1): {"bean":"Phone","property":"number","value":"1234567890123456","message": "..."}, (through reference chain: Person["phones"]->java.util.ArrayList[0])
Looking at the reason, those wrapped in a list do not throw ConstructionViolationException, but throw JsonMappingException.
below dependencies
plugins {
id("org.springframework.boot") version "2.4.3"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.4.30"
kotlin("plugin.spring") version "1.4.30"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
class BeanValidationDeserializer(base: BeanDeserializerBase?) : BeanDeserializer(base) {
private val logger = LoggerFactory.getLogger(this::class.java)
private val validator = Validation.buildDefaultValidatorFactory().validator
override fun deserialize(parser: JsonParser?, ctxt: DeserializationContext?): Any {
val instance = super.deserialize(parser, ctxt)
validate(instance)
return instance
}
private fun validate(instance: Any) {
val violations = validator.validate(instance)
if (violations.isNotEmpty()) {
val message = StringBuilder()
message.append("JSON object is not valid. Reasons (").append(violations.size).append("): ")
for (violation in violations) {
message.append("{\"bean\":\"${violation.rootBeanClass.name}\",")
.append("\"property\":\"${violation.propertyPath}\",")
.append("\"value\":\"${violation.invalidValue}\",")
.append("\"message\": \"${violation.message}\"}")
.append(", ")
}
logger.warn(message.toString())
throw ConstraintViolationException(message.toString(), violations)
}
}
}
#Bean
fun objectMapper(): ObjectMapper = Jackson2ObjectMapperBuilder.json()
.featuresToDisable(MapperFeature.DEFAULT_VIEW_INCLUSION)
.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.modules(ParameterNamesModule(), JavaTimeModule(), Jdk8Module(), KotlinModule(), customValidationModule())
.build()
#Bean
fun customValidationModule(): SimpleModule {
val validationModule = SimpleModule()
validationModule.setDeserializerModifier(object : BeanDeserializerModifier() {
override fun modifyDeserializer(
config: DeserializationConfig?,
beanDesc: BeanDescription?,
deserializer: JsonDeserializer<*>?
): JsonDeserializer<*>? {
return if (deserializer is BeanDeserializer) {
BeanValidationDeserializer(deserializer as BeanDeserializer?)
} else deserializer
}
})
return validationModule
}
I'm not sure how to do it. I ask for your help.
I would say an easier and more maintainable way would be to define a JSON Schema.
After that is in place, you can use one of the two json validation libraries mentioned here (https://json-schema.org/implementations.html#validator-kotlin) to validate your json.
The answer by #rbs is good but requires the overhead of creating json schema per json you want to validate.
Seems like object mapper can be configured not to wrap the exceptions it throws with JsonMappingException. - https://github.com/FasterXML/jackson-databind/issues/2033
All you need to do is to disable WRAP_EXCEPTIONS feature for the objectmapper
Note you cant choose a specific exception type, it will not wrap ALL the exceptions.

Kotlin commonMain with java.io.Serializable

Problem
I have a data class in commonMain (called Person) that I would like to access from jvmMain as type java.io.Serializable.
I have a solution, which is shown below, but I was wondering if this is the best approach. I also found that the library kotlinx.serialization exists, but I'm not sure if it can be a solution.
Current code and solution: expected and actual types
This code works fine, although the required DummyInterface may be a bit useless.
// CommonMain
expect interface Serializable
data class Person(val name: String) : Serializable
// jsMain
interface DummyInterface
actual typealias Serializable = DummyInterface
//jvmMain
actual typealias Serializable = java.io.Serializable
fun main(args: Array<String>) {
val p1: java.io.Serializable = Person("abc")
println(p1)
}
Tried and failed code with kotlinx.serialization
// gradle.kotlin.kts
plugins {
application
kotlin("multiplatform") version "1.4.32"
kotlin("plugin.serialization") version "1.4.32"
}
repositories {
mavenCentral()
}
kotlin {
jvm {
compilations.all {
kotlinOptions.jvmTarget = "11"
}
withJava()
}
js(IR) {
binaries.executable()
browser {}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
}
}
val jvmMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0")
}
}
}
}
// commonMain/kotlin/Person.kt
import kotlinx.serialization.*
#Serializable
data class Person(val name: String)
// jvmMain/kotlin/main.kt
fun main(args: Array<String>) {
// Fails with: "Type mismatch: inferred type is Person but Serializable was expected"
val p1: java.io.Serializable = Person("abc")
println(p1)
}
I know why it fails with a type mismatch, but I would hoping that the kotlinx.serialization plugin would magically add the interface java.io.Serializable to the Person data class.
Question
Is solution with expected and actual types, the best solution for this problem?
Would kotlinx.serialization also be able to provide a solution? If so, what code should I alter?
kotlinx.serialization wasn't exactly developed as an java.io.Serializable abstraction or something. It's a purely kotlin serialization library, for serializing/deserializing JSON objects.
Yes, your first approach is a proper solution I'd say.
There is a similar implementation for Parcelize, you could check out moko-parcelize, it's doing the same thing.