Home > Software design >  Is there an interface applied to serializable types in Kotlin?
Is there an interface applied to serializable types in Kotlin?

Time:12-20

I want to create a class that looks roughly like this:

class MyWrapperClass<T: IsSerializable>(val someData: T) {
  fun stringifyMe(): String {
    return Json.encodeToString(someData)
  }
  

}

My question is about that IsSerializable type. I'd like to accept only types that have been marked as @Serializable, so that I can then call Json.encodeToString() on them. Is this possible?

I tried the code as noted, but I got the error "Cannot use 'T' as reified type parameter. Use a class instead.".

CodePudding user response:

The way that Kotlin Serialisation works is that the serialisation code for each serialisable thing is generated in the companion object of the class, by processing the @Serializable annotation at compile time.

So something like

@Serializable
class Foo(val name: String)

becomes

@Serializable
class Foo(val name: String) {
    companion object {
        fun serializer(): KSerializer<Foo> {
            // implementation details...
            // returns a serializer for Foos in some way 
        }
    }
}

Interfaces can't require a type to have a companion object. Interfaces can only require things related to instances of a type, after all. So there is no interface for this.

As is said in this Kotlin forums comment, one way you could guarantee a successful serialisation is to require the caller to pass in the generated serializer():

class MyWrapperClass<T>(
    val someData: T, 
    val strategy: SerializationStrategy<T>
) {
    fun stringifyMe(): String {
        return Json.encodeToString(strategy, someData)
    }
}

If the caller has a serialisable type, then they could do:

MyWrapperClass(someData, SomeSerializableType.serializer())

Even if a type is not marked with @Serializable, but the caller has got a SerializationStrategy for it using some other means, it is also possible to serialise the instance, and so is by definition, "serialisable".


I don't recommend this, but it is also possible to declare these two interfaces:

interface SerializationStrategyProvider<T> {
    fun serializer(): SerializationStrategy<T>
}

interface MySerializable<T: MySerializable<T>> {
    val serializationStrategyProvider: SerializationStrategyProvider<T>
}

Then your wrapper class can just have one parameter:

class MyWrapperClass<T: MySerializable>(val someData: T) {
    fun stringifyMe(): String {
        return Json.encodeToString(someData.serializationStrategyProvider.serializer(), someData)
    }
}

But the downside is of course that the callers need to implement this interface before they can use your class, and you cannot enforce how they implement this interface. The expected way is:

@Serializable
data class Foo(val bar: String): MySerializable<Foo> {
    companion object: SerializationStrategyProvider<Foo>
    override val serializationStrategyProvider
        get() = Foo
}
  • Related