Kotlin sealed interface serialization - json

Kotlin sealed interface serialization seems to be broken, unless using an explicitly registered serialization module. Am I missing something?
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import kotlin.test.Test
sealed interface iFoo {
#Serializable
#SerialName("a")
class A() : iFoo//Foo(),
}
class EventTest {
#Test
fun testKotlinLang() {
// // this works
// val json = Json { serializersModule = SerializersModule {
// polymorphic(iFoo::class) {
// subclass(iFoo.A::class, iFoo.A.serializer())
// } } }
val json = Json
val a = iFoo.A() as iFoo
val s = json.encodeToString(a)
val a1: iFoo = json.decodeFromString(s)
}
}
Class 'A' is not registered for polymorphic serialization in the scope of 'iFoo'.
Mark the base class as 'sealed' or register the serializer explicitly.
kotlinx.serialization.SerializationException: Class 'A' is not registered for polymorphic serialization in the scope of 'iFoo'.
Mark the base class as 'sealed' or register the serializer explicitly.
at kotlinx.serialization.internal.AbstractPolymorphicSerializerKt.throwSubtypeNotRegistered(AbstractPolymorphicSerializer.kt:102)
at kotlinx.serialization.internal.AbstractPolymorphicSerializerKt.throwSubtypeNotRegistered(AbstractPolymorphicSerializer.kt:113)
at kotlinx.serialization.PolymorphicSerializerKt.findPolymorphicSerializer(PolymorphicSerializer.kt:109)
at kotlinx.serialization.json.internal.StreamingJsonEncoder.encodeSerializableValue(StreamingJsonEncoder.kt:224)
at kotlinx.serialization.json.Json.encodeToString(Json.kt:85)
at io.almer.EventTest.testKotlinLang(EventTest.kt:64)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at
..............
Note that I am using the multiplatform mode but the JVM run fails.

Apparently it's not implemented yet :(
https://github.com/Kotlin/kotlinx.serialization/issues/1417

Possible work-around using sealed interface with #JvmInline: https://github.com/Kotlin/kotlinx.serialization/issues/1417#issuecomment-844300565

Related

Kotlin Serialization issues: Class is not registered for polymorphic serialization in the scope of its interface

I am facing issues with serialization using Kotlin. I've followed through the steps here https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serialization-guide.md but unfortunately, no luck...
This is my code:
sealed interface Convertible {
fun convertUserInput(value : String): String
}
#Serializable
#SerialName("CustomConvertible")
class CustomConvertible(): Convertible {
override fun convertUserInput(value : String): String {
return ""
}
}
#Serializable
class DTOAttribute(val convertibles : List<Convertible> = emptyList())
Later on, I'd like to encode the DTOAttribute with val string = Json.encodeToString(dtoAttr)
Calling this, gives me the following exception:
kotlinx.serialization.SerializationException: Class 'CustomConvertible' is not registered for polymorphic serialization in the scope of 'Convertible'.
Mark the base class as 'sealed' or register the serializer explicitly.
This confuses me, as I've marked the interface as sealed and used #Serializable.
Versions build.gradle.kts
plugins{
kotlin("jvm")
kotlin("plugin.serialization") version("1.6.10")
// ...
}
sourceSets{
named("main") {
dependencies {
// ...
api("org.jetbrains.kotlin:kotlin-reflect:1.6.10")
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
}
}
}
What else am I missing then?
Update: you're using Kotlin 1.6.10, but KxS didn't
support serializing sealed interfaces until 1.6.20
If you can update to 1.6.20+, then adding #Serializable to Convertible works.
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
#Serializable
sealed interface Convertible {
fun convertUserInput(value: String): String
}
#Serializable
#SerialName("CustomConvertible")
class CustomConvertible : Convertible {
override fun convertUserInput(value: String): String {
return ""
}
}
#Serializable
class DTOAttribute(
val convertibles: List<Convertible> = emptyList()
)
fun main() {
val dtoAttribute = DTOAttribute(listOf(CustomConvertible()))
val string = Json.encodeToString(dtoAttribute)
println(string)
// {"convertibles":[{"type":"CustomConvertible"}]}
}
If I remove #Serializable I get the same error that you report
//#Serializable
sealed interface Convertible {
fun convertUserInput(value: String): String
}
Exception in thread "main" kotlinx.serialization.SerializationException: Class 'CustomConvertible' is not registered for polymorphic serialization in the scope of 'Convertible'.
Mark the base class as 'sealed' or register the serializer explicitly.
Versions
Kotlinx Serialization 1.3.3
Kotlin/JVM 1.7.10
Workaround for 1.6.10 - sealed class
If you can't update your version of Kotlin then you can convert Convertible to be a sealed class.
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
#Serializable
sealed class Convertible {
abstract fun convertUserInput(value: String): String
}
#Serializable
#SerialName("CustomConvertible")
class CustomConvertible : Convertible() {
override fun convertUserInput(value: String): String {
return ""
}
}
#Serializable
class DTOAttribute(
val convertibles: List<Convertible> = emptyList()
)
fun main() {
val dtoAttribute = DTOAttribute(listOf(CustomConvertible()))
val string = Json.encodeToString(dtoAttribute)
println(string)
// {"convertibles":[{"type":"CustomConvertible"}]}
}
Setup
Here's how to setup using Gradle Kotlin DSL (from the README):
// build.gradle.kts
plugins {
kotlin("jvm") version "1.7.10" // or kotlin("multiplatform") or any other kotlin plugin
kotlin("plugin.serialization") version "1.7.10"
}
dependencies {
implementation(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.3.3"))
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json")
}

Polymorphic deserialization with kotlinx.serialization in Kotlin/Native

I want to decode a json string containing a list of objects in a polymorphic class structure using kotlinx.serialization in a Kotlin Multiplatform project, but it works only on JVM, not on Native. Here is a minimum reproducible example:
#Serializable
abstract class Project {
abstract val name: String
}
#Serializable
#SerialName("BasicProject")
data class BasicProject(override val name: String): Project()
#Serializable
#SerialName("OwnedProject")
data class OwnedProject(override val name: String, val owner: String) : Project()
fun main() {
val data = Json.decodeFromString<List<Project>>("""
[
{"type":"BasicProject","name":"example"},
{"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"}
]
"""))
}
This works on JVM but throws the following exception on Native:
kotlinx.serialization.SerializationException: Serializer for class ‘Project’ is not found.
Mark the class as #Serializable or provide the serializer explicitly.
On Kotlin/Native explicitly declared serializer should be used for interfaces and enums without #Serializable annotation.message
This problem has been discussed before in the context of encoding and some workarounds have been suggested, e.g. here, but my problem is decoding. Is there a workaround, or do I simply have to implement my own json parser?
You need to explicitly pass respectful serializer and serializersModule:
object ListOfProjectSerializer : KSerializer<List<Project>> by ListSerializer(Project.serializer())
val module = SerializersModule {
polymorphic(Project::class) {
subclass(BasicProject::class)
subclass(OwnedProject::class)
}
}
fun main() {
val data = Json { serializersModule = module }.decodeFromString(
ListOfProjectSerializer,
"""
[
{"type":"BasicProject","name":"example"},
{"type":"OwnedProject","name":"kotlinx.serialization","owner":"kotlin"}
]
"""
)
}

Exception when deserializing JSON in Kotlin JS app

I'm trying to get a Kotlin JS app working, and when consuming data from a server using this code:
val client = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer()
}
}
#Serializable
data class Entry(val start: String, val end: String)
suspend fun loadData() {
val data = client.get<List<Entry>>("http://localhost:8080/data") {
accept(ContentType.Application.Json)
}
console.log(data)
}
I get exceptions like this:
Serializer for class 'Entry' is not found.
Mark the class as #Serializable or provide the serializer explicitly.
On Kotlin/JS explicitly declared serializer should be used for interfaces and enums without #Serializable annotation
even though the class is marked as #Serializable.
If I change it to client.get<List<Map<String,String>>> then I get a valid result.
What am I doing wrong?
I needed to add this to build.gradle.kts:
kotlin("plugin.serialization") version "1.4.31"

Serialize Sealed class within a data class using Gson in kotlin

I have created a sealed class for the json field Value under CustomAttribute data class. This field can return String or Array of Strings.
How can we deserialize this sealed class from json?
data class CustomAttribute (
val attributeCode: String,
val value: Value
)
sealed class Value {
class StringArrayValue(val value: List<String>) : Value()
class StringValue(val value: String) : Value()
}
One solution is to use a RuntimeTypeAdapterFactory as per the instructions in this answer
val valueTypeAdapter = RuntimeTypeAdapter.of(Value::class.java)
.registerSubtype(StringArrayValue::class.java)
.registerSubtype(StringValue::class.java)
val gson = GsonBuilder().registerTypeAdapter(valueTypeAdapter).create()
RuntimeTypeAdapter is included in the source code for Gson but not exposed as a Maven artifact.
It is designed to be copy/pasted into your project from here
I created a TypeAdapterFactory implementation specifically to support sealed classes and their subtypes. This works similarly to the RuntimeTypeAdapterFactory (and I used it as a guide to write my class), but will specifically only support sealed types, and will deserialize using object instances of objects with a sealed class supertype (RuntimeTypeAdapterFactory will create a new instance of object types, which breaks equality checks when a single instance is the expectation).
private class SealedTypeAdapterFactory<T : Any> private constructor(
private val baseType: KClass<T>,
private val typeFieldName: String
) : TypeAdapterFactory {
private val subclasses = baseType.sealedSubclasses
private val nameToSubclass = subclasses.associateBy { it.simpleName!! }
init {
if (!baseType.isSealed) throw IllegalArgumentException("$baseType is not a sealed class")
}
override fun <R : Any> create(gson: Gson, type: TypeToken<R>?): TypeAdapter<R>? {
if (type == null || subclasses.isEmpty() || subclasses.none { type.rawType.isAssignableFrom(it.java) }) return null
val elementTypeAdapter = gson.getAdapter(JsonElement::class.java)
val subclassToDelegate: Map<KClass<*>, TypeAdapter<*>> = subclasses.associateWith {
gson.getDelegateAdapter(this, TypeToken.get(it.java))
}
return object : TypeAdapter<R>() {
override fun write(writer: JsonWriter, value: R) {
val srcType = value::class
val label = srcType.simpleName!!
#Suppress("UNCHECKED_CAST") val delegate = subclassToDelegate[srcType] as TypeAdapter<R>
val jsonObject = delegate.toJsonTree(value).asJsonObject
if (jsonObject.has(typeFieldName)) {
throw JsonParseException("cannot serialize $label because it already defines a field named $typeFieldName")
}
val clone = JsonObject()
clone.add(typeFieldName, JsonPrimitive(label))
jsonObject.entrySet().forEach {
clone.add(it.key, it.value)
}
elementTypeAdapter.write(writer, clone)
}
override fun read(reader: JsonReader): R {
val element = elementTypeAdapter.read(reader)
val labelElement = element.asJsonObject.remove(typeFieldName) ?: throw JsonParseException(
"cannot deserialize $baseType because it does not define a field named $typeFieldName"
)
val name = labelElement.asString
val subclass = nameToSubclass[name] ?: throw JsonParseException("cannot find $name subclass of $baseType")
#Suppress("UNCHECKED_CAST")
return (subclass.objectInstance as? R) ?: (subclassToDelegate[subclass]!!.fromJsonTree(element) as R)
}
}
}
companion object {
fun <T : Any> of(clz: KClass<T>) = SealedTypeAdapterFactory(clz, "type")
}
}
Usage:
GsonBuilder().registerTypeAdapter(SealedTypeAdapterFactory.of(Value::class)).create()
I have successfully serialized and de-serialized a sealed class in the past, with a disclaimer of using Jackson, not Gson as my serialization engine.
My sealed class has been defined as:
#JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS, include = JsonTypeInfo.As.PROPERTY, visible = true)
sealed class FlexibleResponseModel
class SnapshotResponse(val collection: List<EntityModel>): FlexibleResponseModel()
class DifferentialResponse(val collection: List<EntityModel>): FlexibleResponseModel()
class EventDrivenResponse(val collection: List<EntityEventModel>): FlexibleResponseModel()
class ErrorResponse(val error: String): FlexibleResponseModel()
With the annotations used, it required no further configuration for the Jackson instance to properly serialize and de-serialize instances of this sealed class granted that both sides of the communication possessed a uniform definition of the sealed class.
While I recognise that JsonTypeInfo is a Jackson-specific annotation, perhaps you might consider switching over from Gson if this feature is a must - or you might be able to find an equivalent configuration for Gson which would also include the class identifier in your serialized data.

How to serialize/deserialize Kotlin sealed class?

I have a following sealed class:
sealed class ViewModel {
data class Loaded(val value : String) : ViewModel()
object Loading : ViewModel()
}
How can I serialize/deserialize instances of the ViewModel class, let's say to/from JSON format?
I've tried to use Genson serializer/deserializer library - it can handle Kotlin data classes, it's also possible to support polymorphic types (eg. using some metadata to specify concrete types).
However, the library fails on Kotlin object types, as these are singletons without a public constructor. I guess I could write a custom Genson converter to handle it, but maybe there's an easier way to do it?
You are probably right about the creating a custom serializer.
I have tried to serialize and de-serialize your class using the Jackson library and Kotlin.
These are the Maven dependencies for Jackson:
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.8.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.8</version>
</dependency>
You can serialize the sealed class to JSON using this library with no extra custom serializers, but de-serialization requires a custom de-serializer.
Below is the toy code I have used to serialize and de-serialize your sealed class:
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
sealed class ViewModel {
data class Loaded(val value: String) : ViewModel()
object Loading : ViewModel()
}
// Custom serializer
class ViewModelDeserializer : JsonDeserializer<ViewModel>() {
override fun deserialize(jp: JsonParser?, p1: DeserializationContext?): ViewModel {
val node: JsonNode? = jp?.getCodec()?.readTree(jp)
val value = node?.get("value")
return if (value != null) ViewModel.Loaded(value.asText()) else ViewModel.Loading
}
}
fun main(args: Array<String>) {
val m = createCustomMapper()
val ser1 = m.writeValueAsString(ViewModel.Loading)
println(ser1)
val ser2 = m.writeValueAsString(ViewModel.Loaded("test"))
println(ser2)
val deserialized1 = m.readValue(ser1, ViewModel::class.java)
val deserialized2 = m.readValue(ser2, ViewModel::class.java)
println(deserialized1)
println(deserialized2)
}
// Using mapper with custom serializer
private fun createCustomMapper(): ObjectMapper {
val m = ObjectMapper()
val sm = SimpleModule()
sm.addDeserializer(ViewModel::class.java, ViewModelDeserializer())
m.registerModule(sm)
return m
}
If you run this code this is the output:
{}
{"value":"test"}
ViewModel$Loading#1753acfe
Loaded(value=test)
I had a similar problem recently (although using Jackson, not Genson.)
Assuming I have the following:
sealed class Parent(val name: String)
object ChildOne : Parent("ValOne")
object ChildTwo : Parent("ValTwo")
Then adding a JsonCreator function to the sealed class:
sealed class Parent(val name: String) {
private companion object {
#JsonCreator
#JvmStatic
fun findBySimpleClassName(simpleName: String): Parent? {
return Parent::class.sealedSubclasses.first {
it.simpleName == simpleName
}.objectInstance
}
}
}
Now you can deserialize using ChildOne or ChildTwo as key in your json property.
I ended up implementing a custom Converter plus a Factory to properly plug it into Genson.
It uses Genson's metadata convention to represent the object as:
{
"#class": "com.example.ViewModel.Loading"
}
The converter assumes useClassMetadata flag set, so serialization just needs to mark an empty object. For deserialization, it resolves class name from metadata, loads it and obtains objectInstance.
object KotlinObjectConverter : Converter<Any> {
override fun serialize(objectData: Any, writer: ObjectWriter, ctx: Context) {
with(writer) {
// just empty JSON object, class name will be automatically added as metadata
beginObject()
endObject()
}
}
override fun deserialize(reader: ObjectReader, ctx: Context): Any? =
Class.forName(reader.nextObjectMetadata().metadata("class"))
.kotlin.objectInstance
.also { reader.endObject() }
}
To make sure that this converter is applied only to actual objects, I register it using a factory, that tells Genson when to use it and when to fall back to the default implementation.
object KotlinConverterFactory : Factory<Converter<Any>> {
override fun create(type: Type, genson: Genson): Converter<Any>? =
if (TypeUtil.getRawClass(type).kotlin.objectInstance != null) KotlinObjectConverter
else null
}
The factory can be used to configure Genson via builder:
GensonBuilder()
.withConverterFactory(KotlinConverterFactory)
.useClassMetadata(true) // required to add metadata during serialization
// some other properties
.create()
The code probably could be even nicer with chained converters feature, but I didn't have time to check it out yet.
No need for #JsonCreator and sealdSubClass. Jackson has this support in its jackson-module-kotlin, just need one annotation #JsonTypeInfo(use = JsonTypeInfo.Id.NAME):
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
sealed class SuperClass{
class A: SuperClass()
class B: SuperClass()
}
...
val mapper = jacksonObjectMapper()
val root: SuperClass = mapper.readValue(json)
when(root){
is A -> "It's A"
is B -> "It's B"
}
The above example is copied from the its main repo README: https://github.com/FasterXML/jackson-module-kotlin