kotlinx.serialization icon indicating copy to clipboard operation
kotlinx.serialization copied to clipboard

"Serializer for subclass not found" for nested sealed class with custom serializer

Open PatrickHum-at opened this issue 1 year ago • 1 comments

Describe the bug

I'm running into an issue where I need to use a JsonContentPolymorphicSerializer on a nested sealed class. The children of the nested sealed class share the same top level discriminator, and are differentiated by their content.

To Reproduce

@Serializable
sealed class Base {
    @Serializable
    @SerialName("a")
    data class A(val value: Int) : Base()

    @Serializable(with = InnerBase.Companion::class)
    @SerialName("b")
    sealed class InnerBase : Base() {
        @Serializable
        data class D(val value: Int) : InnerBase()

        @Serializable
        data class E(val value: Int) : InnerBase()

        companion object : JsonContentPolymorphicSerializer<InnerBase>(InnerBase::class) {
            override fun selectDeserializer(element: JsonElement): DeserializationStrategy<InnerBase> {
                return if ("foo" in element.jsonObject) {
                    D.serializer()
                } else {
                    E.serializer()
                }
            }
        }
    }
}

class NestedCustomSerializerTest {
    val json = Json { }

    @Test
    fun `test deserialize D`() {
        val actual = json.decodeFromString<Base>(
            """{
            "type": "b",
            "foo": 1,
            "value": 2
            }""".trimMargin()
        )
        assertEquals(Base.InnerBase.D(2), actual)
    }

    @Test
    fun `test deserialize A`() {
        val actual = json.decodeFromString<Base>(
            """{
            "type": "a",
            "value": 7
            }""".trimMargin()
        )
        assertEquals(Base.A(7), actual)
    }
}

When running test deserialize D I get the following error:

kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 0: Serializer for subclass 'b' is not found in the polymorphic scope of 'Base' at path: $
Check if class with serial name 'b' exists and serializer is registered in a corresponding SerializersModule.
To be registered automatically, class 'b' has to be '@Serializable', and the base class 'Base' has to be sealed and '@Serializable'.
JSON input: {
            "type": "b",
            "foo": 1,
            "value": 2
            }
	at app//kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24)
	at app//kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:32)
	at app//kotlinx.serialization.json.internal.AbstractJsonLexer.fail(AbstractJsonLexer.kt:598)
	at app//kotlinx.serialization.json.internal.AbstractJsonLexer.fail$default(AbstractJsonLexer.kt:596)
	at app//kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:85)
	at app//kotlinx.serialization.json.Json.decodeFromString(Json.kt:107)

Expected behavior

The JSON is deserialized to Base.InnerBase.D(2).

Environment

  • Kotlin version: 1.9.23
  • Library version: 1.6.3
  • Kotlin platforms: JVM (Android)
  • Gradle version: 8.6

PatrickHum-at avatar Apr 11 '24 21:04 PatrickHum-at

We did find a workaround using SerializersModule but the default behavior seems odd.

 polymorphicDefaultDeserializer(Base::class) { className ->
        when (className) {
            "b" -> Base.InnerBase.Companion
            else -> error("Unknown type $className")
        }
    }

PatrickHum-at avatar Apr 11 '24 22:04 PatrickHum-at