kotlinx.serialization: Inconsistent behaviour with sealed classes serialization

I faced a problem with sealed classes serialization.

To Reproduce

import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

@Serializable
sealed class Project {
	abstract val name: String

	@Serializable
	class OwnedProject(override val name: String, val owner: String) : Project()
}

fun main() {
	println(Json.encodeToString(Project.OwnedProject("kotlinx.coroutines", "kotlin")))
	// prints: {"name":"kotlinx.coroutines","owner":"kotlin"}
	val project: Project = Project.OwnedProject("kotlinx.coroutines", "kotlin")
	// prints: {"type":"com.example.Project.OwnedProject","name":"kotlinx.coroutines","owner":"kotlin"}
	println(Json.encodeToString(project))
}

I thought that result in both cases should be equal, but it prints different JSON strings.

Environment

  • Kotlin version: 1.4.10
  • Library version : Kotlinx Serialization Plugin 1.4.10
  • Kotlin platforms: JVM
  • Gradle version: 6.6.1
  • Other relevant context: JRE version: 1.8

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 7
  • Comments: 19 (10 by maintainers)

Most upvoted comments

and therefore “knows” the type and doesn’t need to do any polymorphic handling

@pdvrieze just because we know the concrete type on serializer side at this time doesn’t mean the deserializer will know what to do, right?

You can pass the base class type explicitly in as the type argument. Alternatively, you store the value in a variable with the type of the sealed base.

You can also use Json.encodeToString<Project>(Project.OwnedProject("kotlinx.coroutines", "kotlin"))

The workarounds mentioned here are not possible for me as I don’t control the place where Kotlinx.Serialization is called. I’m using Spring support for kotlinx serialization, and internally Spring uses serializer<T>() to find the serializer for the object I give to it.

The interface I have with SimpMessagingTemplate is a generic one (independent from the message converters used) taking just the destination topic, and the payload:

simpMessagingTemplate.convertAndSend("/topic/games", GameListEvent.CreateOrUpdate(game))

The problem is that this means it always has the concrete type, so it never finds it “necessary” to include the type information.

But that is when Spring sends messages over the STOMP websocket. Now when I receive them in the topic I use, I expect any subclass of my sealed class, so the deserializer doesn’t know what to use and fails. I think it would be necessary to have a parameter to include the discriminator all the time.

One workaround that would work is actually wrapping the sealed class into a concrete type with a single field, because the type of the field would be the sealed class parent and solve this problem. But that’s pretty inconvenient to use. It would be much nicer to have a global switch to enable consistent serialization of polymorphic classes.

Is there any way to enable a discriminator field to all class hierarchy forcefully? I mean like JsonTypeInfo annotation for Jackson mapper or something? If I remember correctly Jackson includes a discriminator field in both cases.

My case is related to response serialization in “ktor” route. Because of this problem, it looks like this:

	post("/login") {
		val request = call.receive<LoginCredentials>()
		val response: TokenResponse = authService.login(request)
		call.respond(Json.encodeToString(response))
	}

I have to do manual encoding through encodeToString and have to specify the “needed” response type explicitly.