kotlinx-serialization: Polymorphic serializer was not found for missing class discriminator ('null')
Asked Answered
E

2

18

I am trying to serialize a json, but its throwing JsonDecodingException. Check the code:

SerializationTestCase.kt:

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import org.junit.Test

class SerializationTestCase {

    private val json = Json {
        ignoreUnknownKeys = true
        isLenient = true
        prettyPrint = true
        allowStructuredMapKeys = true
        encodeDefaults = true
        classDiscriminator = "#class"
        serializersModule = SerializersModule {
            polymorphic(Module::class) {
                subclass(TeamModule::class, TeamModule.serializer())
            }
        }
    }

    @Test
    fun test() {
        val inputJson = """
    [
        {
          "type": "team",
          "data": {
            "readOnly": true
          }
        }
    ]
""".trimIndent()

        val moduleList: List<Module> = json.decodeFromString(inputJson)
        println(moduleList)
    }
}


@Serializable
sealed class Module(val type: String)

@Serializable
@SerialName("TeamModule")
data class TeamModule(val data: TeamData) : Module("team")

@Serializable
data class TeamData(
    val readOnly: Boolean = false
)

Error Log:

kotlinx.serialization.json.internal.JsonDecodingException: Polymorphic serializer was not found for missing class discriminator ('null')
JSON input: {"type":"team","data":{"readOnly":true}}

    at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24)
    at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:32)
    at kotlinx.serialization.json.internal.PolymorphicKt.throwSerializerNotFound(Polymorphic.kt:80)
    at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:70)
    at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:32)
    at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableValue(AbstractDecoder.kt:43)
    at kotlinx.serialization.encoding.AbstractDecoder.decodeSerializableElement(AbstractDecoder.kt:70)
    at kotlinx.serialization.encoding.CompositeDecoder$DefaultImpls.decodeSerializableElement$default(Decoding.kt:536)
    at kotlinx.serialization.internal.ListLikeSerializer.readElement(CollectionSerializers.kt:80)
    at kotlinx.serialization.internal.AbstractCollectionSerializer.readElement$default(CollectionSerializers.kt:51)
    at kotlinx.serialization.internal.AbstractCollectionSerializer.merge(CollectionSerializers.kt:36)
    at kotlinx.serialization.internal.AbstractCollectionSerializer.deserialize(CollectionSerializers.kt:43)
    at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:63)
    at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:32)
    at kotlinx.serialization.json.Json.decodeFromString(Json.kt:85)
    at com.arinspect.domain.SerializationTestCase.test(SerializationTestCase.kt:61)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
    at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
    at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

I went through the documentation and I am doing the same as asked, but still its not working as expected. Any help would be appreciated!

PS: Dependency I am using:

api "org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0"

and kotlin version is 1.4.31

Looks like this is an issue, I have reported this here: https://github.com/Kotlin/kotlinx.serialization/issues/1382

Efficiency answered 18/3, 2021 at 12:1 Comment(2)
Why is your serializersModule referencing InspectionModule? It's nowhere else in your code snippet.Travancore
@Travancore Oops. Sorry, my bad. I updated the question.Efficiency
E
26

We can use JsonContentPolymorphicSerializer.

According to documentation:

Base class for custom serializers that allows selecting polymorphic serializer without a dedicated class discriminator, on a content basis.

this is how I achieved:

class SerializationTestCase {

    private val json = Json {
        ignoreUnknownKeys = true
        isLenient = true
        prettyPrint = true
        encodeDefaults = true
        classDiscriminator = "#class"
    }

    @Test
    fun test() {
        val inputJson = """
    [
    {"type": "team","data": {"readOnly": true}},
    {"type": "org","data": {"readOnly": true}}
    ]
""".trimIndent()

        val moduleList: List<Module> =
            json.decodeFromJsonElement(json.parseToJsonElement(inputJson))
        println(moduleList)
    }
}


@Serializable(with = ModuleSerializer::class)
sealed class Module {
    abstract val type: String
}

@Serializable
data class TeamModule(val data: TeamData, override val type: String = "team") : Module()

@Serializable
data class OrgModule(val data: TeamData, override val type: String = "org") : Module()

@Serializable
data class TeamData(
    val readOnly: Boolean = false
)

object ModuleSerializer : JsonContentPolymorphicSerializer<Module>(Module::class) {
    override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Module> {
        return when (element.jsonObject["type"]?.jsonPrimitive?.content) {
            "team" -> TeamModule.serializer()
            "org" -> OrgModule.serializer()
            else -> throw Exception("Unknown Module: key 'type' not found or does not matches any module type")
        }
    }
}
Efficiency answered 19/3, 2021 at 3:9 Comment(2)
In this case classDiscriminator = "#class" is not used and should be omitted.Revalue
how to add ModuleSerializer to the jsonWoodcraft
B
2

For those who seek to implement this without using JsonContentPolymorphicSerializer, you can utilize the @SerialName annotation on each class specifying correct discriminator value and let the kotlinx.serialization do the processing.

class SerializationTestCase {

    private val json = Json {
        ignoreUnknownKeys = true
        isLenient = true
        prettyPrint = true
        allowStructuredMapKeys = true
        encodeDefaults = true
        classDiscriminator = "type" // default is "type", so you can just omit it
    }

    @Test
    fun test() {
        val inputJson = """
    [
        {
          "type": "team",
          "data": {
            "readOnly": true
          }
        }
    ]
""".trimIndent()

        val moduleList: List<Module> = json.decodeFromString(inputJson)
        println(moduleList)
    }
}


@Serializable
sealed class Module {
    abstract val type: String
}

@Serializable
@SerialName("team") // <-- this is the class discriminator. It should match the value of the `type` property in the JSON.
data class TeamModule(override val type: String, val data: TeamData) : Module()

@Serializable
@SerialName("org") // <-- this is the class discriminator. It should match the value of the `type` property in the JSON.
data class OrgModule(override val type: String, val data: TeamData) : Module()

@Serializable
data class TeamData(
    val readOnly: Boolean = false
)
Breadthways answered 12/5, 2023 at 19:54 Comment(1)
Please be sure NOT to add fields to the parent's CONSTRUCTOR (in this case Module class). Instead, only add them as abstract val properties, the same way as in the example. You could even annotate them with @SerialName for common fields. Children MUST use default empty constructor, otherwise it won't work.Parabola

© 2022 - 2024 — McMap. All rights reserved.