Kotlinx.serialization: Inheritance/polymorphic serialization

Created on 9 Apr 2019  路  8Comments  路  Source: Kotlin/kotlinx.serialization

I have the following hierarchy:

@Serializable
sealed class Search(@SerialName("type") private var searchType: SearchType,
                    @Transient open val table: String = "",
                    @Transient open val index: String? = null,
                    @Transient open val filters: List<QueryCondition> = listOf(),
                    @Transient open val order: Order = Order.DESC) {
    fun isAscOrdered(): Boolean {
        return order == Order.ASC
    }
}

@Serializable
class QuerySearch(override val table: String,
                       override val index: String?,
                       val keys: List<QueryCondition>,
                       override val filters: List<QueryCondition>,
                       override val order: Order) : Search(SearchType.QUERY, table, index, filters, order) {
...
}

@Serializable
class ScanSearch(override val table: String,
                 override val index: String?,
                 override val filters: List<QueryCondition>) :
        Search(SearchType.SCAN, table, index, filters, Order.ASC) {
...
}

And it already looks "ugly" in order to do the proper serialization, i.e. have to mark parent properties as open, and override them (in order to make them properties, as required by the serialization library) in the children.

Also in order to not have parent properties duplicated in the output JSON, have to mark them @Transient, but it then requires to set the default values (where there are no really meaningful default values).

Only then it produces the proper JSON, like the one below:

{
  "type": "QUERY",
  "table": "Table A",
  "index": "Index A",
  "keys": [
    {
      "name": "Id",
      "type": "NUMBER",
      "operator": "EQ",
      "values": [
        "10"
      ]
    }
  ],
  "filters": [
    {
      "name": "Timestamp",
      "type": "NUMBER",
      "operator": "BETWEEN",
      "values": [
        "100",
        "200"
      ]
    },
    {
      "name": "Name",
      "type": "STRING",
      "operator": "BEGINS_WITH",
      "values": [
        "Test"
      ]
    }
  ],
  "order": "ASC"
}

Although when deserialize the JSON back to the class, the parent properties are set to the default values instead of the values from the child class.

And moreover don't know how to do the polymorphic deserialization, i.e. using the discriminator value, like "type" property.

Is then this library suitable for the above use case, or it is the ultimate end goal, but it is not ready yet to handle this?

Or this library was specifically designed in mind to work only with data classes?

Here is the unit test to produce the JSON above:

class SearchSpec : StringSpec({
    "serialize query search" {
        val search = QuerySearch(
                "Table A",
                "Index A",
                listOf(QueryCondition("Id", Type.NUMBER, Operator.EQ, listOf("10"))),
                listOf(QueryCondition("Timestamp", Type.NUMBER, Operator.BETWEEN, listOf("100", "200")),
                        QueryCondition("Name", Type.STRING, Operator.BEGINS_WITH, listOf("Test"))),
                Order.ASC)
        val data = Json.stringify(QuerySearch.serializer(), search)
        println(data)
        val parse = Json.parse(Search.serializer(), data) // Obviously this line fails, 
        // val parse = Json.parse(QuerySearch.serializer(), data) // but this works, but results in the default properties for the parent Search class
        parse shouldBe search
    }
})
design question

Most helpful comment

Inheritance doesn't seem to work at all...

import kotlinx.serialization.Serializable

@Serializable
open class Fruit(
  val name: String? = null
)

@Serializable
class Apricot(
  name: String
): Fruit(name)

==> IllegalStateException: Class Apricot have constructor parameters which are not properties and therefore it is not serializable automatically

AND

import kotlinx.serialization.Serializable

@Serializable
open class Fruit(
  open val name: String? = null
)

@Serializable
class Apricot(
  override val name: String
): Fruit(name)

==> IllegalStateException: class Apricot has duplicate serial name of property name, either in it or its parents.

All 8 comments

How I really would like it to see is to be able to write the following:

@Serializable
sealed class Search(@SerialName("type") private var searchType: SearchType,
                    val table: String = "",
                    val index: String? = null,
                    protected val filters: List<QueryCondition> = listOf(),
                    private val order: Order = Order.DESC) {
    fun isAscOrdered(): Boolean {
        return order == Order.ASC
    }
}

@Serializable
class QuerySearch(table: String,
                       index: String?,
                       keys: List<QueryCondition>,
                       filters: List<QueryCondition>,
                       order: Order) : Search(SearchType.QUERY, table, index, filters, order) {
...
}

And it works. I.e. be able to figure the properties from the parent class and apply them to the child class. Plus extra annotation to describe the discriminator property/polymorphic behavior.

We have this goal in mind. Next release of the library will relax some restrictions, e.g. properties which do not have backing fields (an abstract ones) would be implicitly transient, and there would be a setting in JSON format to include class discriminator directly into JSON object (#50).

The one big limitation left is the requirement that @Serializable class constructor should have only vals or vars so the framework can create object back.

Has this ever worked?

import kotlinx.serialization.Serializable

@Serializable
sealed class Search(
  @SerialName("type") private var searchType: Int,
  @Transient open val table: String = "",
)

@Serializable
class QuerySearch(
  override val table: String,
  override val index: String?,
) : Search(123, table)

because with 0.11.0 it produces :

e: java.lang.IllegalStateException: class QuerySearch has duplicate serial name of property table, either in it or its parents.
    at org.jetbrains.kotlinx.serialization.compiler.resolve.SerializableProperties.validateUniqueSerialNames(SerializableProperties.kt:59)
    at org.jetbrains.kotlinx.serialization.compiler.resolve.SerializableProperties.<init>(SerializableProperties.kt:54)
    at org.jetbrains.kotlinx.serialization.compiler.resolve.KSerializerDescriptorResolver.createLoadConstructorDescriptor(KSerializerDescriptorResolver.kt:296)
    at org.jetbrains.kotlinx.serialization.compiler.extensions.SerializationResolveExtension.generateSyntheticSecondaryConstructors(SerializationResolveExtension.kt:79)

Inheritance doesn't seem to work at all...

import kotlinx.serialization.Serializable

@Serializable
open class Fruit(
  val name: String? = null
)

@Serializable
class Apricot(
  name: String
): Fruit(name)

==> IllegalStateException: Class Apricot have constructor parameters which are not properties and therefore it is not serializable automatically

AND

import kotlinx.serialization.Serializable

@Serializable
open class Fruit(
  open val name: String? = null
)

@Serializable
class Apricot(
  override val name: String
): Fruit(name)

==> IllegalStateException: class Apricot has duplicate serial name of property name, either in it or its parents.

Any updates on this topic ?

Bit of a hijack, but is there a current way to support just the type string deserialisation portion of this issue? Ignoring the ergonomics of data class / property declaration etc, is there a way to deserialise to a class from a known string that _isnt_ the class name?

The current behavior of forcing a class name means you need to expose class names to your consumers..? Something like Jacksons type-id system https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization#2-on-type-ids

Oh great! That鈥檚 a well hidden nugget, and here was me digging around the code of the builder 馃檮

Thanks!

Was this page helpful?
0 / 5 - 0 ratings