I'm trying to make an Event publisher that can tak...
# serialization
a
I'm trying to make an Event publisher that can take any serializable type as a payload and send it off to the event endpoint that we have, but I'm not sure how to do that without the failure
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
. Here is my attached code, but essentially I want a single function that can publish all my events and every time I want to create a new type of event, I can just create a class that extends it with the right payload type.
Copy code
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import <http://io.ktor.client.request.post|io.ktor.client.request.post>
import io.ktor.client.request.setBody
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json


@Serializable
sealed class Event<T>(
    val message: String, val payload: T
) {
    @Serializable
    data class UserCreated(
        val user: User
    ) : Event<User>(
        "User created", user
    )
}

@Serializable
data class User(
    val id: String,
    val userName: String
)

object EventPublisher {
//    private val client = HttpClient(CIO)

    suspend fun <T> publish(event: Event<T>) {
        println(Json.encodeToString(event))     // Represents the client call below which also doesn't work
//        <http://client.post|client.post>("<http://127.0.0.1:8080/events>") {
//            setBody(event)
//        }
    }
}

suspend fun main() {
    val user = User("id", "username")
    EventPublisher.publish(Event.UserCreated(user))
}
And I'm using the following dependencies:
Copy code
implementation(platform("io.ktor:ktor-bom:2.0.0-beta-1"))
implementation("io.ktor:ktor-client-core")
implementation("io.ktor:ktor-client-content-negotiation")
implementation("io.ktor:ktor-client-serialization")
implementation("io.ktor:ktor-client-cio")
with the kotlinx-serialization plugin
n
tried to reproduce.. you seem to be hitting some IR compiler bug.. that certainly seems weird
a
Were you able to reproduce or did it work for you? I can push a repo to github if necessary
n
this works for me..
Copy code
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.serializer

@Serializable
open class Box<T>(val contents: T)

@Serializable
data class StringBox<T>(
    private val data: T
) : Box<T>(data) where T: String

fun main() {
    println(
        Json.encodeToString(
//            StringBox.serializer(String.serializer()),
            StringBox("Kotlin")
        )
    )
}
the trick is that it seems to only be able to use generics when passed all the way up..
definitly seems like a bug.. i generally try to just avoid generics.. and at most just have sealed serializable things..
a
Do you know of any way to do what I was originally trying to do (second message in thread) where I have multiple Events each with a different payload and a single function that takes an Event, encodes it to json, and prints/sends it to an endpoint? It just doesn't make sense to make a new publish function for every type of Event
n
well in case of that userevent.. i would write it like so
Copy code
@Serializable
data class UserCreated<USER>(
    val user: USER
) : Event<USER>(
    "User created", user
) where USER : User
and kotlinx-serialization should deal with it properly
there probably is some magic annotations or contextual serialization that could solve this too.. i wonder..
from all the testing i did.. seems like the compiler (plugin) just fails when you do not pass all the generics through..
a
This is what I've come to so far:
Copy code
@Serializable
open class Event<T>(val message: String, val contents: T)

@Serializable
class UserCreatedEvent<T>(
    private val user: T
) : Event<T>("StringEvent", user) where T: User

@Serializable
class UserUpdatedEvent<T>(
    private val oldUser: T,
    private val newUser: T
) : Event<Diff<T>>("StringEvent", Diff(oldUser, newUser)) where T: User

@Serializable
data class Diff<T>(
    val old: T,
    val new: T
)

@Serializable
data class User(
    val id: String,
    val name: String
)
It allows me to run the following:
Copy code
fun main() {
    val user = User("id", "Alice")
    println(Json.encodeToString(UserCreatedEvent(user)))

    val updatedUser = user.copy(name = "Bob")
    println(Json.encodeToString(UserUpdatedEvent(user, updatedUser)))
}
but not this
Copy code
fun main() {
    val user = User("id", "Alice")
    println(EventPublisher.publish(UserCreatedEvent(user)))

    val updatedUser = user.copy(name = "Bob")
    println(EventPublisher.publish(UserUpdatedEvent(user, updatedUser)))
}

object EventPublisher {
//    private val client = HttpClient(CIO)

    fun <T> publish(event: Event<T>) {
        println(Json.encodeToString(event))     // Represents the client call below which also doesn't work
//        <http://client.post|client.post>("<http://127.0.0.1:8080/events>") {
//            setBody(event)
//        }
    }
}
n
this.. somehow works?
Copy code
@Serializable
sealed class Event<T>(
    val message: String,
) {
    abstract val payload: T

    @Serializable
    data class UserCreated(
        val user: User,
    ) : Event<User>(
        "User created"
    ) {
        override val payload = user
    }
}
i do not know why it makes a difference but it works
a
Seems like the main issue now is when calling the
EventPublisher.publish
function
n
you could make the function inline reified.. maybe that helps ?
i changed the eventpublisher to this:
Copy code
object EventPublisher {
//    private val client = HttpClient(CIO)

    @OptIn(InternalSerializationApi::class)
    suspend inline fun <reified T> publish(event: Event<T>, json: Json = Json.Default) {
        println(Json.encodeToString(
            event
        ))
        // Represents the client call below which also doesn't work
//        <http://client.post|client.post>("<http://127.0.0.1:8080/events>") {
//            setBody(event)
//        }
    }
}
^^ that fixes your last example for me
a
I'm trying to figure out what I changed, I'm getting
kotlinx.serialization.SerializationException: Class 'User' is not registered for polymorphic serialization in the scope of 'User'.
with the change you suggested
Ah, it seems that when the
Event
class is sealed, I get that
SerializationException
, but when it's open, I don't. I wonder if that's a separate bug that needs to be reported.
n
adjusted the last version a little.. passing the serializer along also works but needs some weird generic in the publish function..
Copy code
import kotlinx.serialization.*
import kotlinx.serialization.json.Json

@Serializable
abstract class Event<T>(
    val message: String,
) {
    abstract val contents: T
}

@Serializable
class UserCreatedEvent(
    private val user: User
) : Event<User>("StringEvent") {
    override val contents = user
}

@Serializable
class UserUpdatedEvent(
    private val oldUser: User,
    private val newUser: User
) : Event<Diff<User>>("StringEvent") {
    override val contents = Diff(oldUser, newUser)
}

@Serializable
data class Diff<T>(
    val old: T,
    val new: T
)

@Serializable
data class User(
    val id: String,
    val name: String
)

object EventPublisher {
    @OptIn(InternalSerializationApi::class)
    inline fun <reified EVENT: Event<T>, T> publish(event: EVENT, serializer: KSerializer<EVENT>, json: Json = Json.Default): String {
        return json.encodeToString(
            serializer,
            event
        )
    }
}

suspend fun main() {
    val user = User("id", "Alice")
    println(EventPublisher.publish(UserCreatedEvent(user), UserCreatedEvent.serializer()))

    val updatedUser = user.copy(name = "Bob")
    println(EventPublisher.publish(UserUpdatedEvent(user, updatedUser), UserUpdatedEvent.serializer()))
}
could be simplified i guess
a
Thanks for so much for your help! I think I need to learn the
where
,
inline
, and
reified
keywords better. I'll try this on the actual code I was writing which has a few more layers of indirection and get back to you if it doesn't work.
n
i think the trick is that generics cannot be in the constructor of the superclass for some reason.. and if they are.. they must also be on all subclasses.. otherwise the compiler breaks and inline reified.. basically inlines the function at the callsite and allows the compiler to use the resolved type as a argument..
but with the magic that serialization does.. with autogenerated serializer functions.. it sometimes works.. or doesn't
139 Views