kotlinkotlinx.serialization

How to properly use class inheritance in Kotlin in combination with Kotlinx Serialization


I have a simple hierarchy containing of the following:

They should all work with Kotlinx Serialization. It went fine until I added the Backpack class. I use version 1.4.32 of Kotlinx Serialization.

Here is my class hierarchy in detail

// Items.kt

@Serializable
sealed class BaseItem {
    abstract val id: String
    abstract val type: ItemType
    abstract var brand: String
    abstract var model: String
    abstract var imageLink: String
    abstract var traits: MutableList<Trait>
    abstract var implicitTraits: MutableList<Trait>
    abstract var details: MutableMap<String, String>
}

@Serializable
open class Item(
    override val id: String = UUID.randomUUID().toString(),
    override val type: ItemType = ItemType.UNDEFINED,
    override var brand: String,
    override var model: String,
    override var imageLink: String,
    override var traits: MutableList<Trait>,
    override var implicitTraits: MutableList<Trait>,
    override var details: MutableMap<String, String>,
) : BaseItem()

@Serializable // is marked as incorrect 
class Backpack(
    brand: String,
    model: String,
    imageLink: String,
    traits: MutableList<Trait>,
    implicitTraits: MutableList<Trait>,
    details: MutableMap<String, String>,
    var volume: Int
) : Item(
    type = ItemType.BACKPACK,
    brand = brand,
    model = model,
    imageLink = imageLink,
    traits = traits,
    implicitTraits = implicitTraits,
    details = details
)

The IDE is showing me the following message for the @Serialization annotation attached to the Backpack class.

This class is not serializable automatically because it has primary constructor parameters that are not properties

I was not able to find out what the working way it is to make this correct


Solution

  • It's because the parameters of your constructor are not defined as properties of the class. To have the parameters defined as properties you have to add val or var to the parameters. This would resolve the error message you currently have:

    @Serializable
    class Backpack(
        override var brand: String,
        override var model: String,
        override var imageLink: String,
        override var traits: MutableList<Trait>,
        override var implicitTraits: MutableList<Trait>,
        override var details: MutableMap<String, String>,
        var volume: Int
    ) : Item(
        type = ItemType.BACKPACK,
        brand = brand,
        model = model,
        imageLink = imageLink,
        traits = traits,
        implicitTraits = implicitTraits,
        details = details
    )
    

    But this would still not compile as you would end up with Serializable class has duplicate serial name of property 'brand', either in the class itself or its supertypes for all properties that are used in both classes. But anyhow, I am a little bit surprised by the design as it usually not a good practice to inherit from a non abstract class. Without knowing the intentions I am wondering whether sth.like this would not work for you as well:

    sealed class BaseItem {
        abstract val id: String
        abstract val type: ItemType
        abstract var brand: String
        abstract var model: String
        abstract var imageLink: String
        abstract var traits: MutableList<Trait>
        abstract var implicitTraits: MutableList<Trait>
        abstract var details: MutableMap<String, String>
    }
    
    @Serializable
    data class Item(
        override val id: String = UUID.randomUUID().toString(),
        override val type: ItemType = ItemType.UNDEFINED,
        override var brand: String,
        override var model: String,
        override var imageLink: String,
        override var traits: MutableList<Trait>,
        override var implicitTraits: MutableList<Trait>,
        override var details: MutableMap<String, String>,
    ) : BaseItem()
    
    @Serializable
    data class Backpack(
        override val id: String = UUID.randomUUID().toString(),
        override val type: ItemType = ItemType.BACKPACK,
        override var brand: String,
        override var model: String,
        override var imageLink: String,
        override var traits: MutableList<Trait>,
        override var implicitTraits: MutableList<Trait>,
        override var details: MutableMap<String, String>,
        override var var volume: Int
    ) : BaseItem()
    

    I btw. removed the @Serializable from BaseItem as it is unnecessary because the class is abstract anyhow and therefore won't be serialized at all. I also made your classes data class because my impression was that those basically exist to hold data and those are usually implemented with data class. I left the many var I see as I don't know the reasoning for those but a small hint from my side is that you should prefer val over var especially in data class. A var in this scenario feels like a code smell to me and you might want to consider using val instead. A good literatur for such things is the Kotlin page itself: https://kotlinlang.org/docs/coding-conventions.html#idiomatic-use-of-language-features