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

Kotlinx deserialization does not include properties from abstract class when using “JsonContentPolymorphicSerializer”

Open iskugor opened this issue 3 years ago • 5 comments

Describe the bug

When using JsonContentPolymorphicSerializer to deserialize class which extends abstract class, properties from super class are missing.

To Reproduce

Lets say there is abstract class BaseClass ...

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable(with = PolymorphicSerializer::class)
abstract class BaseClass {
    @SerialName("bid")
    var baseId: String? = null
 }

... class ExtendedClass that extends it ...

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
class ExtendedClass(
     @SerialName("eid")
     var newId: String? = null
) : BaseClass()

... and PolymorphicSerializer

import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement

object PolymorphicSerializer  : JsonContentPolymorphicSerializer<BaseClass>(BaseClass::class) {
    override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out BaseClass> {
        return ExtendedClass.serializer()
    }
}

PolymorphicSerializer is used because JSON can be deserialized as different classes that extend BaseClass based on some property (for sake of simplicity I just used ExtendedClass.serializer() )

Now, if we try to deserialize generic JSON to ExtendedClass ...

class DeserializationTest {
    @Test
    fun deserialization_isCorrect() {
        val extendedClass = Json {
            ignoreUnknownKeys = true
        }.decodeFromString<BaseClass>("""{"bid":"bid","eid":"eid"}""")

        assertEquals("bid", extendedClass.baseId)
    }
}

... test fails

java.lang.AssertionError: Expected :bid Actual :null

If I try to serialize ExtendedClass ...

val extendedClass = ExtendedClass()
extendedClass.baseId = "bid"
extendedClass.newId = "eid"

val encodedJson = Json {
    ignoreUnknownKeys = true
}.encodeToString(extendedClass)

I can see encodedJson is {"eid":"eid"} but if I remove JsonContentPolymorphicSerializer this works fine (replace @Serializable(with = PolymorphicSerializer::class) with @Serializable ), encodedJson is {"bid":"bid","eid":"eid"}

So, for some reason JsonContentPolymorphicSerializer does not take into account properties from super class.

Expected behavior

JsonContentPolymorphicSerializer should take into account properties from super class, that is, extendedClass.baseId should not be null, it should be "bid".

I managed to solve this by declaring baseId as abstract:

@SerialName("bid")
abstract var baseId: String?

and then in ExtendedClass

@SerialName("bid")
 override var baseId: String?

But that solution is a bit ugly considering the fact we need to define it in every class that extends BaseClass. Not sure if this is a bug, but it would be nice to have solution that does not require that.

Environment

  • Kotlin version: 1.4.32
  • Library version: 1.1.0
  • Kotlin platforms: JVM
  • Gradle version: 4.1.3
  • IDE version: Android Studio 4.1.3
  • Other relevant context: MacOS 11.0.1, Java(TM) SE Runtime Environment (build 1.8.0_25-b17)

iskugor avatar Apr 23 '21 13:04 iskugor

We are having the same issue (with a sealed base class), the workaround is quite ugly, so a fix would be great :)

  • Kotlin version: 1.5.10
  • Library version: 1.1.0
  • Kotlin platforms: JVM
  • Other relevant context: Windows 10

KevinBassaDevelopment avatar Jun 25 '21 07:06 KevinBassaDevelopment

I'm running into the same kind of design issue with JsonContentPolymorphicSerializer. It makes working with large hierarchies with several shared properties almost impossible without writing lots of ugly code repetitions. Any updates on this? Thank you! 🙏

What's more, not only properties need to be overridden on each subclass, but any @SerialName or @Serializable annotation also needs to be repeated 🤦

marcosalis avatar May 17 '22 13:05 marcosalis

I think the root of the problem here is @Serializable(with = PolymorphicSerializer::class) on the BaseClass itself. See, normally ExtendedClass (de)serializer would delegate to the generated serializer of the BaseClass for filling its own properties (including private ones). When Serializable annotation is applied with serializer override plugin does not do this delegation anymore, since it simply doesn't know what kind of serializer is used for the base class. Hence, the base class properties are lost.

I think this is one more use-case for #1169. In the meantime, a workaround would be to avoid @Serializable(with = MyPolymorphicSerializer::class) directly on the class and use other methods of passing serializer instead (@file:UseSerializers, etc)

sandwwraith avatar Jun 20 '22 14:06 sandwwraith