Search code examples
kotlingenericskmm

Implementing generic method in interface that uses implementors class


I want a method on a parent Interface / Abstract Class that makes use of generics methods to pass in the class of the implementing class.

interface Domain {
    fun toJSON(): String { return Json.encodeToString(this) }
}

@Serializable
class User: Domain {
    val a: Int
}

This doesn't work since Json.encodeToString doesn't know the class of 'this'.

@Serializable seems to implement KSerializer so in theory I could require domain to descend from it, but that interface is templated. And marking the implementing class @Serializable doesn't seem to implement KSerializer until compile time so creates errors.

How do I implement this toJSON() method or tell Domain that its implementers must be @Serializable / KSerializer?

I have also tried:

interface Domain<T> {
    fun toJSON(): String { return Json.encodeToString(this) }
}

@Serializable
class User: Domain<User> {
    val a: Int
}

But this results in:

kotlin.IllegalStateException: Only KClass supported as classifier, got T

One additional complication in all this is that I'm attempting to do this in KMM.


Solution

  • It does not work, because encodeToString() resolves the type at compile time, not at runtime (it uses reified type).

    Maybe there is a better way, but you can do this by acquiring a serializer manually, resolving the type at runtime:

    fun toJSON(): String {
        val serializer = Json.serializersModule.serializer(this::class.createType())
        return Json.encodeToString(serializer, this)
    }
    

    Note this will only work on JVM. If you need to do this in KMM then be aware that multiplatform reflection is complicated and/or it needs some time to mature. kotlinx.serialization provides a way to do this, but there are warnings about possible inconsistent behavior between platforms:

    @OptIn(InternalSerializationApi::class)
    fun toJSON(): String {
        @Suppress("UNCHECKED_CAST")
        val serializer = this::class.serializer() as KSerializer<Any?>
        return Json.encodeToString(serializer, this)
    }