Home > Blockchain >  Implementing generic method in interface that uses implementors class
Implementing generic method in interface that uses implementors class

Time:10-13

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.

CodePudding user response:

Json.encodeToString uses a reified generic to access the class of the parameter. It means it uses the declared type of the parameter (known at compile time), which here is Domain.

The simplest approach would be to just use your own generic reified extension function to capture the declared class of the receiver more precisely:

interface Domain

inline fun <reified T : Domain> T.toJSON(): String = Json.encodeToString(this)

@Serializable
data class User(
    val name: String,
    val age: Int,
): Domain

fun main() {
    val user = User("Bob", 35)
    println(user.toJSON())
}

Note, however, that this suffers from the same problem: if you try to use this extension on a variable declared with Domain type, it still won't try to access the runtime type:

val user: Domain = User("Bob", 35)
println(user.toJSON()) // still a problem here

If you want to actually use the dynamic type, you can access the serializer dynamically instead like @broot mentioned in his answer.

CodePudding user response:

@Serializable seems to implement KSerializer

That's not true. @Serializable is an annotation that instructs the annotation processor to generate (among other stuff) a serializer() method (for companion object), which returns an instance of KSerializer for this class. It doesn't add new interfaces for the class itself, so you can't tell the type system what it should check.

If a serializer won't be found, you'll get a runtime error:

import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

class HasNoSerializableAnnotation

fun main() {
    // Compiles fine, but will throw runtime exception
    // kotlinx.serialization.SerializationException: Serializer for class 'HasNoSerializableAnnotation' is not found.
    Json.encodeToString(HasNoSerializableAnnotation()) 
}

That's intentional because it's possible to serialize instances of classes without a plugin-generated serializer (even if you don't pass custom serializer manually into encodeToXXX method as another parameter - via passing serializer (as contextual) in serializersModule).

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

There is no way to do this. You can have this API, but it's runtime-errors prone:

interface Domain
inline fun <reified T : Domain> T.toJSON() = Json.encodeToString(this)

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

Actually, you don't need whole KSerializer here - just its serialization part:

interface Domain
inline fun <reified T : Domain> T.toJSON(serializer: SerializationStrategy<T>) = 
    Json.encodeToString(serializer,this)

CodePudding user response:

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)
}
  • Related