Serializer for interface / implementation - kotlin

Suppose there is one interface , and 2 (or more) implementations :
interface IRunnable {
fun run()
}
class Horse : IRunnable {
override fun run() { println("horse running") }
}
class Dog : IRunnable {
override fun run() { println("dog running") }
}
I want to achieve the most succinct JSON for network transmission, that is , {"runnable":"H"} for Horse and {"runnable":"D"} for Dog
If I cannot modify the interface and implementations , I want to serialize the interface/implementations to JSON , according to Kotlin's documentation , I have to write a custom serializer , and use SerializersModule to achieve the goal.
Suppose there are only Horse and Dog implementing IRunnable , This is what I've done :
class RunnableSerializer : KSerializer<IRunnable> {
override val descriptor: SerialDescriptor
get() = StringDescriptor.withName("runnable")
override fun serialize(encoder: Encoder, obj: IRunnable) {
val stringValue = when (obj) {
is Horse -> { "H" }
is Dog -> { "D" }
else -> { null }
}
stringValue?.also {
encoder.encodeString(it)
}
}
override fun deserialize(decoder: Decoder): IRunnable {
return decoder.decodeString().let { value ->
when(value) {
"H" -> Horse()
"D" -> Dog()
else -> throw RuntimeException("invalid $value")
}
}
}
}
But when I try to transform a Horse to JSON ...
class RunnableTest {
private val logger = KotlinLogging.logger { }
#ImplicitReflectionSerializer
#Test
fun testJson() {
val module1 = serializersModuleOf(IRunnable::class, RunnableSerializer())
Json(context = module1).stringify(IRunnable::class.serializer(), Horse()).also {
logger.info("json = {}", it)
}
}
}
It outputs error :
kotlinx.serialization.SerializationException:
Can't locate argument-less serializer for class IRunnable. For generic classes, such as lists, please provide serializer explicitly.
How to achieve something like
{"runnable":"H"} or {"runnable":"D"} in this case ?
Thanks.
environments :
<kotlin.version>1.3.60</kotlin.version>
<serialization.version>0.14.0</serialization.version>
updated , full error message :
kotlinx.serialization.SerializationException: Can't locate argument-less serializer for class IRunnable. For generic classes, such as lists, please provide serializer explicitly.
at kotlinx.serialization.PlatformUtilsKt.serializer(PlatformUtils.kt:12)
at RunnableTest.testJson(RunnableTest.kt:17)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

What you are trying to achieve is called polymorphic serialization, which kotlinx.serialization supports. In short: it requires you to register all deriving types of IRunnable as polymorphic in the SerialModule. For your code it should look somewhat as follows:
val serialModule = SerializersModule {
polymorphic(IRunnable::class) {
Horse::class with HorseSerializer
Dog::class with DogSerializer
}
}
Currently, you only register IRunnable, which as the exception correctly indicates, cannot be initialized. How would you initialize an interface? You can't. Instead, you want to know which specific deriving type to initialize. This requires to embed type information within the JSON data, which is what polymorphic serialization does.
In addition, you do not necessarily have to implement a custom serializer. As per the documentation you link to, you can create external serializers for library classes. The following might suffice, as the kotlinx.serialization compiler plugin will try to generate a suitable serializer for you, similar to if you were to add #Serializable on the class directly:
#Serializer(forClass = Horse::class)
object HorseSerializer {}
#Serializer(forClass = Dog::class)
object DogSerializer {}

Related

Serializer for sealed interface (kotlinx.serialization)

I am trying to serialize my base class that is implementing two sealed interfaces. I have tried multiple approaches, yet i always get the error :
caused by: kotlinx.serialization.SerializationException: Class 'PayloadFromBuilder' is not registered for polymorphic serialization in the scope of 'Payload'.
Mark the base class as 'sealed' or register the serializer explicitly.
I was following mostly this guide Kotlinx/polymorphism and checked some similar questions here.
My code:
sealed inteface MyClass {
dataetc
}
#Serializable
private class DefaultMyClass(dataetc): MyClass
fun MyClass(dataetc): MyClass = DefaultMyClass
Sealed interface MyClassBuilder {
fun dataetc(value: ByteArray)
fun dataetc(value: ByteArray)
fun dataetc(value: ByteArray?)
}
#PublishedApi
#Serializable
#SerialName("payload")
internal class MyClassFromBuilder: MyClassBuilder, MyClass {
}
//Serialization
val module = SerializersModule {
polymorphic(MyClass::class) {
subclass(MyClassFromBuilder::class, MyClassFromBuilder.serializer())
default { MyClassFromBuilder.serializer() }
}
polymorphic(MyClassBuilder::class) {
subclass(MyClassFromBuilder::class, MyClassFromBuilder.serializer())
default { MyClassFromBuilder.serializer() }
}
}
val ConfiguredProtoBuf = ProtoBuf { serializersModule = module }
#ExperimentalSerializationApi
internal inline fun <reified T> ProtoBuf.encodeToMessage(value: T): Message =
Message(encodeToByteArray(value))
From what i have seen i think i am very close to the solution yet i am missing something, since my example is very generic if you need more info let me know, thank you in advance.
Note: In my several tries i have tried to annotate both sealed intefaces with #Polymorphic but i am not sure if it changed anything.
Note 2: My code breaks when i am calling the encodeToMessage fun
So i messed big time, turns out i was not using my ConfiguredProtoBuf when i was calling my encodeToMessage

Add attribute to XML without POJO modification

I need to supply shared secret attribute to XML, so I decided to add it without exposing it to my API.
#JacksonXmlRootElement(localName = "Request")
data class TestRequest(#JacksonXmlText val request: String)
Here is example POJO, after serializer it looks like
<Request>text</Request>
I need to add attribute to it, like
<Request secret="foobar">text</Request>
I looked to Jackson API and it looks like I need to create custom serializer for root, so
class SessionModule: SimpleModule("Test serializer", PackageVersion.VERSION) {
override fun setupModule(context: SetupContext) {
context.addBeanSerializerModifier(object : XmlBeanSerializerModifier() {
override fun modifySerializer(config: SerializationConfig, beanDesc: BeanDescription, serializer: JsonSerializer<*>): JsonSerializer<*> {
val modifiedSerializer = super.modifySerializer(config, beanDesc, serializer)
if (modifiedSerializer is XmlBeanSerializer) {
println("Registering custom serializer")
return SessionFieldSerializer(modifiedSerializer)
}
return modifiedSerializer
}
})
}
}
And my custom serializer that do nothing
class SessionFieldSerializer: XmlBeanSerializer {
constructor(src: BeanSerializerBase?) : super(src)
constructor(src: XmlBeanSerializerBase?, objectIdWriter: ObjectIdWriter?, filterId: Any?) : super(src, objectIdWriter, filterId)
constructor(src: XmlBeanSerializerBase?, objectIdWriter: ObjectIdWriter?) : super(src, objectIdWriter)
constructor(src: XmlBeanSerializerBase?, toIgnore: MutableSet<String>?) : super(src, toIgnore)
override fun serialize(bean: Any?, g: JsonGenerator?, provider: SerializerProvider?) {
TODO()
}
}
So, all it do is throw not-implemented exception, however even if SessionFieldSerializer() getting instantiated ( I see "Registering custom serializer" message), serialize function is not called.
Test code:
val mapper = XmlMapper()
mapper.registerModule(KotlinModule())
mapper.registerModule(SessionModule())
val request = TestRequest("Foobar")
val test = mapper.writeValueAsString(request)
println(test)
Am I missing something?

Kotlin: Can an abstract super class have an abstract constructor?

I have just written this, which is fine as far as it goes:
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.int
import com.github.salomonbrys.kotson.jsonObject
import com.google.gson.JsonElement
import com.google.gson.JsonObject
abstract class BatchJobPayload {
abstract fun toJson(): JsonObject
}
class BookingConfirmationMessagePayload(val bookingId: Int) : BatchJobPayload() {
constructor(payload: JsonElement) : this(payload["bookingId"].int)
override fun toJson() = jsonObject(
"bookingId" to bookingId
)
}
But I'd like to insist, if possible, that all classes that extend BatchJobPayload implement a secondary constructor with the signature
constructor(payload: JsonElement): BatchJobPayload, which is to be used for deserializing.
BookingConfirmationMessagePayload has such a constructor but only because I put it there, not because BatchJobPayload insisted upon it...
A workable option I came up with as as follows:
interface BatchJobPayload {
fun toJson(): JsonObject
}
interface BatchJobPayloadDeserialize {
operator fun invoke(payload: JsonElement): BatchJobPayload
}
class BookingConfirmationMessagePayload(val bookingId: Int) : BatchJobPayload {
override fun toJson() = jsonObject(
"bookingId" to bookingId
)
}
class BookingConfirmationMessagePayloadDeserialize : BatchJobPayloadDeserialize {
override operator fun invoke(payload: JsonElement) =
BookingConfirmationMessagePayload(payload["bookingId"].int)
}
Now you can deserialize a BookingConfirmationMessagePayload object from a JsonElement as follows:
BookingConfirmationMessagePayloadDeserialize()(payload)
(The invoke operator is just some syntactic sugar here which may border on the obtuse...)
Actually I still prefer the original code which is less verbose --- a developer needing to subclass BatchJobPayload in the future may initially neglect to define a constructor that takes a JsonElement but they will surely realise their omission once they have just a string of JSON which they need to turn into an instance of their new class...
You can't enforce a super constructor, but you can have factories with a spawn method enforced that returns a subclass of BatchJobPayload, which allows you to make sure classes will be constructable.
It would look something like this:
class JsonObject // Included to make compiler happy
abstract class Factory<T> {
abstract fun make(obj: JsonObject): T
}
abstract class Base {
abstract fun toJson(): JsonObject
}
class A(val data:JsonObject):Base() {
override fun toJson(): JsonObject {
return JsonObject()
}
}
class AFactory: Factory<A>() {
override fun make(obj: JsonObject): A {
return A(obj)
}
}
fun main(args: Array<String>) {
val dummyJson = JsonObject()
var factory = AFactory()
var instance = factory.make(dummyJson)
println(instance)
}

Java SAM interface created from Kotlin gives ClassCastException

I have a java method:
addHandler(HttpServiceHandler handler)
HttpServiceHandler is
interface HttpServiceHandler extends Consumer<HttpHandlerContext>
The point is to avoid Consumer<HttpHandlerContext> copy-paste across the project, so it's kind of a type alias.
In Java code, this works all right:
addHandler({ context -> context.blah(); })
Now, in Kotlin, I have this method that generates handlers:
private companion object {
fun newHandler(notimportant: Long): HttpServiceHandler {
return HttpServiceHandler { context -> context.blah() }
}
}
HttpServiceHandler {} is important, it doesn't compile if I don't specify HttpServiceHandler for the lambda.
And this compiles:
addHandler(newHandler(1L))
But at runtime, throws:
java.lang.ClassCastException: blah.BlahTest$Companion$newHandler$1 cannot be cast to kotlin.jvm.functions.Function1
at blah.BlahTest.test(BlahTest.kt:42)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:51)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
And I can't figure out why. Help please?
Update: The class cast exception is gone if I write it like this:
addHandler(
object : HttpServiceHandler {
override fun accept(c: HttpHandlerContext) {
c.complete()
}
}
)
But still throws the exception when written like this:
fun newHandler(blah: Long): HttpServiceHandler {
return object : HttpServiceHandler {
override fun accept(c: HttpHandlerContext) {
c.complete()
}
}
}
addHandler(newHandler(1L))
And I have no idea why.
Update2: Test code at https://github.com/wilem82/testcases/tree/master/kotlinsam1 . Does not reproduce the problem, sadly.
the error has already said exactly:
java.lang.ClassCastException: blah.BlahTest$Companion$newHandler$1 cannot be cast to kotlin.jvm.functions.Function1
you try to cast a Consumer<HttpHandlerContext> to a Function1 in addHandler method or somewhere, for example:
fun addHandler(handler: Consumer<HttpHandlerContext>) {
val it: Function1<*, *> = handler as Function1<*, *>
// ^
// ClassCastException was thrown since it is not a Function1
}
you should using method reference expression in java / function reference expression in kotlin to convert a SAM interface to another SAM interface, for example:
fun addHandler(handler: Consumer<HttpHandlerContext>) {
val it: Function1<HttpHandlerContext, Unit> = handler::accept
// using function reference expression here ---^
//...
}
IF you call java method addHandler in kotlin, you needn't to create such a bridge method newHandler, you can just call it with lambda in kotlin, for example:
addHandler{context->
context.blah()
// additional works ...
}

How to obtain all subclasses of a given sealed class?

Recently we upgraded one of our enum class to sealed class with objects as sub-classes so we can make another tier of abstraction to simplify code. However we can no longer get all possible subclasses through Enum.values() function, which is bad because we heavily rely on that functionality. Is there a way to retrieve such information with reflection or any other tool?
PS: Adding them to a array manually is unacceptable. There are currently 45 of them, and there are plans to add more.
This is how our sealed class looks like:
sealed class State
object StateA: State()
object StateB: State()
object StateC: State()
....// 42 more
If there is an values collection, it will be in this shape:
val VALUES = setOf(StateA, StateB, StateC, StateC, StateD, StateE,
StateF, StateG, StateH, StateI, StateJ, StateK, StateL, ......
Naturally no one wants to maintain such a monster.
In Kotlin 1.3+ you can use sealedSubclasses.
In prior versions, if you nest the subclasses in your base class then you can use nestedClasses:
Base::class.nestedClasses
If you nest other classes within your base class then you'll need to add filtering. e.g.:
Base::class.nestedClasses.filter { it.isFinal && it.isSubclassOf(Base::class) }
Note that this gives you the subclasses and not the instances of those subclasses (unlike Enum.values()).
With your particular example, if all of your nested classes in State are your object states then you can use the following to get all of the instances (like Enum.values()):
State::class.nestedClasses.map { it.objectInstance as State }
And if you want to get really fancy you can even extend Enum<E: Enum<E>> and create your own class hierarchy from it to your concrete objects using reflection. e.g.:
sealed class State(name: String, ordinal: Int) : Enum<State>(name, ordinal) {
companion object {
#JvmStatic private val map = State::class.nestedClasses
.filter { klass -> klass.isSubclassOf(State::class) }
.map { klass -> klass.objectInstance }
.filterIsInstance<State>()
.associateBy { value -> value.name }
#JvmStatic fun valueOf(value: String) = requireNotNull(map[value]) {
"No enum constant ${State::class.java.name}.$value"
}
#JvmStatic fun values() = map.values.toTypedArray()
}
abstract class VanillaState(name: String, ordinal: Int) : State(name, ordinal)
abstract class ChocolateState(name: String, ordinal: Int) : State(name, ordinal)
object StateA : VanillaState("StateA", 0)
object StateB : VanillaState("StateB", 1)
object StateC : ChocolateState("StateC", 2)
}
This makes it so that you can call the following just like with any other Enum:
State.valueOf("StateB")
State.values()
enumValueOf<State>("StateC")
enumValues<State>()
UPDATE
Extending Enum directly is no longer supported in Kotlin. See
Disallow to explicitly extend Enum class : KT-7773.
With Kotlin 1.3+ you can use reflection to list all sealed sub-classes without having to use nested classes: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-class/sealed-subclasses.html
I asked for some feature to achieve the same without reflection: https://discuss.kotlinlang.org/t/list-of-sealed-class-objects/10087
Full example:
sealed class State{
companion object {
fun find(state: State) =
State::class.sealedSubclasses
.map { it.objectInstance as State}
.firstOrNull { it == state }
.let {
when (it) {
null -> UNKNOWN
else -> it
}
}
}
object StateA: State()
object StateB: State()
object StateC: State()
object UNKNOWN: State()
}
A wise choice is using ServiceLoader in kotlin. and then write some providers to get a common class, enum, object or data class instance. for example:
val provides = ServiceLoader.load(YourSealedClassProvider.class).iterator();
val subInstances = providers.flatMap{it.get()};
fun YourSealedClassProvider.get():List<SealedClass>{/*todo*/};
the hierarchy as below:
Provider SealedClass
^ ^
| |
-------------- --------------
| | | |
EnumProvider ObjectProvider ObjectClass EnumClass
| |-------------------^ ^
| <uses> |
|-------------------------------------------|
<uses>
Another option, is more complicated, but it can meet your needs since sealed classes in the same package. let me tell you how to archive in this way:
get the URL of your sealed class, e.g: ClassLoader.getResource("com/xxx/app/YourSealedClass.class")
scan all jar entry/directory files in parent of sealed class URL, e.g: jar://**/com/xxx/app or file://**/com/xxx/app, and then find out all the "com/xxx/app/*.class" files/entries.
load filtered classes by using ClassLoader.loadClass(eachClassName)
check the loaded class whether is a subclass of your sealed class
decide how to get the subclass instance, e.g: Enum.values(), object.INSTANCE.
return all of instances of the founded sealed classes
If you want use it at child class try this.
open class BaseSealedClass(val value: String, val name: Int) {
companion object {
inline fun<reified T:BaseSealedClass> valueOf(value: String): T? {
return T::class.nestedClasses
.filter { clazz -> clazz.isSubclassOf(T::class) }
.map { clazz -> clazz.objectInstance }
.filterIsInstance<T>()
.associateBy { it.value }[value]
}
inline fun<reified T:BaseSealedClass> values():List<T> =
T::class.nestedClasses
.filter { clazz -> clazz.isSubclassOf(T::class) }
.map { clazz -> clazz.objectInstance }
.filterIsInstance<T>()
}
}
#Stable
sealed class Theme(value: String, name: Int): BaseSealedClass(value, name) {
object Auto: Theme(value = "auto", name = R.string.setting_general_theme_auto)
object Light: Theme(value= "light", name = R.string.setting_general_theme_light)
object Dark: Theme(value= "dark", name = R.string.setting_general_theme_dark)
companion object {
fun valueOf(value: String): Theme? = BaseSealedClass.valueOf(value)
fun values():List<Theme> = BaseSealedClass.values()
}
}
For a solution without reflection this is a library that supports generating a list of types to sealed classes at compile time:
https://github.com/livefront/sealed-enum
The example in the docs
sealed class Alpha {
object Beta : Alpha()
object Gamma : Alpha()
#GenSealedEnum
companion object
}
will generate the following object:
object AlphaSealedEnum : SealedEnum<Alpha> {
override val values: List<Alpha> = listOf(
Alpha.Beta,
Alpha.Gamma
)
override fun ordinalOf(obj: Alpha): Int = when (obj) {
Alpha.Beta -> 0
Alpha.Gamma -> 1
}
override fun nameOf(obj: AlphaSealedEnum): String = when (obj) {
Alpha.Beta -> "Alpha_Beta"
Alpha.Gamma -> "Alpha_Gamma"
}
override fun valueOf(name: String): AlphaSealedEnum = when (name) {
"Alpha_Beta" -> Alpha.Beta
"Alpha_Gamma" -> Alpha.Gamma
else -> throw IllegalArgumentException("""No sealed enum constant $name""")
}
}
The short version is
State::class.sealedSubclasses.mapNotNull { it.objectInstance }