How to ignore unknown types in polymorphic deserialization (instead of throwing) ?
What is your use-case and why do you need this feature?
I have several classes that I want to deserialize, that include lists of polymorphic types. I can get it to deserialize correctly known types, but deserializing an unknown type throws an exception. What I really want is that the list includes only known types and unknown types are just filtered out. If this could be done in a generic way would be even better.
sealed interface BaseInterface
interface I1 : BaseInterface {
fun f1(): String
}
interface I2: BaseInterface {
fun f2(): String
}
@Serializable
@SerialName("i1")
data class I1Rest(val value: String): I1 {
override fun f1(): String = value
}
@Serializable
@SerialName("i2")
data class I2Rest(val value: String): I2 {
override fun f2(): String = value
}
@Serializable
data class SomeClass(val list: List<BaseInterface>)
How can I can correctly deserialize SomeClass with
{ "list": [ {"type": "i1", "value": "v1" }, {"type": "i2", "value": "v2" }, {"type": "i3", "value": "v3" } ] }
If I don't add the i3 type to the json, I can correctly deserialize it using
SerializersModule {
polymorphic(BaseInterface::class){
subclass(I1Rest::class)
subclass(I2Rest::class)
}
}
But as soon as I include an unknown type, it breaks. Note that I don't want to deserialize unknowns to a default type (that would have to extend the base sealed interface). What I want is to ignore/filter the unknowns. (preferably in a generic way) I would also would like to keep the BaseInterface as an interface and not a class, because I only want to expose interfaces and not concrete classes (this is for a lib)
I already figured out how to make a List serializer that filters out nulls, with code that someone else posted but I'm currently stuck on how to make a polymorphic (de)serializer for this sealed interface structure, that outputs nulls if finds unknown type (instead of throwing) and still handles the type discriminator when both serializing and deserializing (although serializing an unknown type should not be possible)
Describe the solution you'd like I'd like to know how to ignore or map to null unknown types in polymorphic deserialization while still preserving the use of the type class discriminator in both serialization and deserialization
So, I'm not sure there are better ways to handle this, but this works and is the best I could come up with.
Also it would be nice to if there was a way to register a general handler for List<BaseInterface> without having to annotate every such list with a @Serialized(with= ...) but I haven't found a way.
import kotlinx.serialization.*
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import kotlinx.serialization.modules.subclass
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
class PolymorphicTest {
sealed interface BaseInterface
interface I1 : BaseInterface {
fun f1(): String
}
interface I2 : BaseInterface {
fun f2(): String
}
@Serializable
@SerialName("i1")
data class I1Rest(val value: String) : I1 {
override fun f1(): String = value
}
@Serializable
@SerialName("i2")
data class I2Rest(val value: String) : I2 {
override fun f2(): String = value
}
@Serializable
data class SomeClass(
@Serializable(with = UnknownBaseInterfaceTypeFilteringListSerializer::class)
val list: List<BaseInterface>
)
val json = Json {
ignoreUnknownKeys = true
serializersModule = SerializersModule {
polymorphic(BaseInterface::class) {
subclass(I1Rest::class)
subclass(I2Rest::class)
defaultDeserializer { UnknownTypeSerializer() }
}
}
}
class FilteringListSerializer<E>(private val elementSerializer: KSerializer<E>) : KSerializer<List<E>> {
private val listSerializer = ListSerializer(elementSerializer)
override val descriptor: SerialDescriptor = listSerializer.descriptor
override fun serialize(encoder: Encoder, value: List<E>) {
listSerializer.serialize(encoder, value)
}
override fun deserialize(decoder: Decoder): List<E> = with(decoder as JsonDecoder) {
decodeJsonElement().jsonArray.mapNotNull {
try {
json.decodeFromJsonElement(elementSerializer, it)
} catch (e: UnknownTypeException) {
null
}
}
}
}
class UnknownTypeException(message: String) : SerializationException(message)
open class UnknownTypeSerializer<T> : KSerializer<T> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Nothing")
override fun deserialize(decoder: Decoder): T = throw UnknownTypeException("unknown type")
override fun serialize(encoder: Encoder, value: T) = throw UnknownTypeException("unknown type")
}
object UnknownBaseInterfaceTypeFilteringListSerializer : KSerializer<List<BaseInterface>> by FilteringListSerializer(PolymorphicSerializer(BaseInterface::class))
private fun commonAssertions(input: String) {
val someClass = json.decodeFromString<SomeClass>(input)
assertEquals("v1", (someClass.list[0] as I1).f1())
assertEquals("v2", (someClass.list[1] as I2).f2())
val expected = """{"list":[{"type":"i1","value":"v1"},{"type":"i2","value":"v2"}]}"""
assertEquals(expected, json.encodeToString(someClass))
}
@Test
fun `all known types`() {
val input = """{"list":[{"type":"i1","value":"v1"},{"type":"i2","value":"v2"}]}"""
commonAssertions(input)
}
@Test
fun `unknown types`() {
val input = """{ "list": [ {"type": "i1", "value": "v1" }, {"type": "i2", "value": "v2" }, {"type": "i3", "value": "v3" } ] }"""
commonAssertions(input)
}
@Test
fun `missing type field`() {
val input = """{ "list": [ {"type": "i1", "value": "v1" }, {"type": "i2", "value": "v2" }, {"not_a_type": "i3", "value": "v3" } ] }"""
commonAssertions(input)
}
@Test
fun `wrong json element type`() {
val input = """{ "list": [ {"type": "i1", "value": "v1" }, {"type": "i2", "value": "v2" }, "not an object" ] }"""
assertThrows<SerializationException> {
json.decodeFromString<SomeClass>(input)
}
}
@Test
fun `right type but incomplete data`() {
val input = """{ "list": [ {"type": "i1", "value": "v1" }, {"type": "i2" } ] }"""
assertThrows<SerializationException> {
json.decodeFromString<SomeClass>(input)
}
}
}
You should be able to register a default polymorphic serializer/deserializer in the serializersModuleBuilder (using polymorphicDefaultDeserializer). If you do, you can write the code that determines the return value (you may need to create a dummy child object that you then filter out or something).
See: https://github.com/Kotlin/kotlinx.serialization/blob/784892467f292dae27d3544148515795cd8eeee8/core/commonMain/src/kotlinx/serialization/modules/SerializersModuleBuilders.kt#L107-L123
One way to do it:
@Serializable
sealed class Event() {
@SerialName("foo")
@Serializable
data class FooEvent(
@SerialName("bar") val bar: Bar
) : Event()
@Serializable
object Unknown : Event()
}
val eventSerializationModule = SerializersModule {
polymorphic(Event::class) {
default { Event.Unknown.serializer() }
}
}
val json = Json {
serializersModule += eventSerializationModule
}
val listOfEvents = json.decodeFromString<List<Event>>(jsonString)
// You can now log and filter unknown events
listOfEvents.forEach {
if (it is Event.Unknown) {
log("Unknown event received!")
} else {
// Process event
}
}
We would also need to have a solution to achieve this using the annotation "JsonClassDiscriminator"
Maybe taking the default serializer the one that doesn't have the SerialName
@Serializable
@JsonClassDiscriminator("kind")
sealed class Base {
@Serializable
@SerialName("i1")
data class MyClass1(
@SerialName("data") val data: String?
) : Base()
@Serializable
@SerialName("i2")
data class MyClass2(
@SerialName("data") val data: String?
) : Base()
@Serializable
data class Unknown(
@SerialName("data") val data: String?
) : Base()
}
Or adding a parameter at the annotation: JsonClassDiscriminator:
@JsonClassDiscriminator("kind", default = Unknown.serializer())
+1 On this, could really do with this feature
Or maybe would be good to have possibility to revert to null