Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JsonDecodingException: Polymorphic serializer was not found for missing class discriminator ('null') #1382

Closed
iChintanSoni opened this issue Mar 18, 2021 · 9 comments
Labels

Comments

@iChintanSoni
Copy link

Describe the bug
Unable to deserialize json string. Upon performing decodeFromString() throws Exception:

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)

To Reproduce
I wrote a test case for demonstrate this:

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
)

Expected behavior
It should deserialize.

Environment

  • Kotlin version: 1.4.31
  • Library version: 1.1.0
  • Kotlin platforms: JVM
  • Gradle version: 6.5
  • IDE version: Android Studio 4.1.2
  • Build #AI-201.8743.12.41.7042882, built on December 20, 2020
  • Runtime version: 1.8.0_242-release-1644-b3-6222593 amd64
  • VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
  • Linux 5.8.0-45-generic
  • GC: ParNew, ConcurrentMarkSweep
  • Memory: 1981M
  • Cores: 8
  • Registry: ide.new.welcome.screen.force=true, external.system.auto.import.disabled=true
  • Non-Bundled Plugins: com.thoughtworks.gauge, com.intellij.marketplace, org.jetbrains.kotlin, com.google.mad-scorecard
  • Current Desktop: ubuntu:GNOME
@iChintanSoni
Copy link
Author

iChintanSoni commented Mar 18, 2021

Deserialization works if I pass classDiscriminator into input json as:

 val inputJson = """
    [
        {
          "class": "TeamModule",
          "type": "team",
          "data": {
            "readOnly": true
          }
        }
    ]
""".trimIndent()

@iChintanSoni
Copy link
Author

Looks like Serialization/Deserialization works only through below steps:

  1. You serialize the Serializable Object (output would be the json string with an additional key of classDiscriminator, "class")
  2. Then, you deserialize the same output string from previous step. This deserialization will work as expected (because now it has the classDiscriminator as key)

@shanshin
Copy link
Contributor

classDiscriminator attribute is necessary when working with inheritance, see https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md
In your case, you using closed polymorphism for sealed classes.

If you do not need polymorphism in this place you can deserialize like this val moduleList: List<TeamModule> = json.decodeFromString(inputJson) otherwise, you need to pass the classDiscriminator attribute value so that the deserializer knows which child class of Module to instantiate.

@iChintanSoni
Copy link
Author

iChintanSoni commented Mar 19, 2021

@shanshin I agree with that. But open polymorphism is the requirement. Is there any way where we can use the discriminator that is already available in Json. We already have a key "type" in core json structure (from Module class). Can you provide a way to use that core json key as discriminator instead of using built-in discriminator or custom classDiscriminator? I tried something like this:

object ModuleSerializer : KSerializer<Module> {
    @OptIn(InternalSerializationApi::class)
    override val descriptor: SerialDescriptor
        get() = buildClassSerialDescriptor("ModuleDescriptor"){
            element<String>("type", isOptional = false)
        }

    override fun deserialize(decoder: Decoder): Module {
        val stringModule = decoder.beginStructure(descriptor)
//        stringModule.decodeElementIndex(descriptor) != 0
//        val jsonModule = json.parseToJsonElement(stringModule.).jsonObject
        return when (stringModule.decodeStringElement(descriptor,0)) {
            "team" -> {
                val module = decoder.decodeSerializableValue(TeamModule.serializer())
                stringModule.endStructure(descriptor)
                module
            }
            else -> {
                throw Exception("Unknown Module")
            }
        }
    }

    override fun serialize(encoder: Encoder, value: Module) {
        encoder.encodeString(value.toString())
    }

}

@sandwwraith
Copy link
Member

I think that JsonContentPolymorphicSerializer is a feature you're looking for: https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#content-based-polymorphic-deserialization

@iChintanSoni
Copy link
Author

@sandwwraith Yes I achieved it using JsonContentPolymorphicSerializer.
Here's link to SO where I answered my own question this morning:
https://stackoverflow.com/questions/66690712/kotlinx-serialization-polymorphic-serializer-was-not-found-for-missing-class-di

@iChintanSoni
Copy link
Author

We can close this issue 👍

@nesteiner
Copy link

@iChintanSoni hey, I follow your code and write this

@Serializable(with = ItemSerializer::class)
sealed class Item {
    abstract val type: String

    @Serializable
    data class A(val number: Int): Item() {
        override val type: String = "A"
    }

    @Serializable
    data class B(val number1: Int, val number2: Int): Item() {
        override val type: String = "B"
    }
}

object ItemSerializer: JsonContentPolymorphicSerializer<Item>(Item::class) {
    override fun selectDeserializer(element: JsonElement): DeserializationStrategy<Item> {
        return when (element.jsonObject["type"]?.jsonPrimitive?.content) {
            "A" -> Item.A.serializer()
            "B" -> Item.B.serializer()
            else -> throw Exception("Unknown Item type")
        }
    }

}

class SealedClassSerializationTest {
    @Test
    fun testSerialization() {
        val formatter = Json {
            encodeDefaults = true
            prettyPrint = true
        }

        val item = Item.A(1)
        val s = Json.encodeToString(item)
        println(s)
        val result = Json.decodeFromString<Item>(s)
        println(result)
    }
}

but I found that
image

it is very strange, right ?

@sandwwraith
Copy link
Member

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants