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