https://kotlinlang.org logo
Title
o

Osman Saral

03/22/2023, 5:41 PM
Hi, I'm trying to implement a DDP client using ktor WebSocket. DDP messages needs to be serialized like this (when doing manually)
val array = arrayOf(Json.encodeToString(obj))
val s = Json.encodeToString(array)

send(Frame.Text(s)) //send this string to the websocket.
I'm using
KotlinxWebsocketSerializationConverter
and I want to do this absurd serialization automatically when call
sendSerialized
of web socket session. Can I write a custom serializer to do this wrapping when I call it like
sendSerialized(obj)
. I know I can use
KSerializer
and use
@Serializable(with = ..
with it, but I don't want to mark every data class I create with it. Is this possible to do? Can anyone lead me the way to do it?
a

Adam S

03/22/2023, 9:24 PM
would it help to make an extension function that encapsulates all of this functionality? like
fun FooKtorContext.sendOb(obj: MyDdpObj) = ...
o

Osman Saral

03/22/2023, 10:45 PM
ofcourse that would work but I wanted learn how it's done actually.
j

Johann Pardanaud

03/23/2023, 9:40 AM
Take a look at the implementation of the KotlinxWebsocketSerializationConverter class, you can make your own implementation with the WebsocketContentConverter interface. But I think, here, the best option would be to create a custom serializer.
o

Osman Saral

03/24/2023, 6:38 AM
I've checked it actually. But it has a a lot of internal class and function, most important one is the
serializerFromTypeInfo
which I want to use. I want to use the default behaviour before I wrap my object with array. This also prevents me to use a custom serializer. I can't use the default serializer in my custom serializer when I use it with
@Serializable(with = ..
j

Johann Pardanaud

03/24/2023, 10:59 AM
I don’t see any limitation, is this what you want?
object NestedJsonSerializer : KSerializer<List<String>> {
    override val descriptor: SerialDescriptor =
        PrimitiveSerialDescriptor("some.NestedJsonSerializer", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: List<String>) = encoder.encodeString(Json.encodeToString(value))

    override fun deserialize(decoder: Decoder): List<String> = Json.decodeFromString(decoder.decodeString())
}

@Serializable
data class SomePayload(
    @Serializable(with = NestedJsonSerializer::class)
    val strings: List<String>,
)

fun main() {
    val payload = SomePayload(listOf("hello", "world"))
    val encoded = Json.encodeToString(payload)
    println(encoded)
    val decoded = Json.decodeFromString<SomePayload>(encoded)
    println(decoded)
}
Output:
{"strings":"[\"hello\",\"world\"]"}
SomePayload(strings=[hello, world])
o

Osman Saral

03/24/2023, 6:47 PM
Here is my implementation:
object DDPMessageSerializer : KSerializer<Outgoing> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("DDPMessageSerializer", PrimitiveKind.STRING)

    private val json = Json {
        encodeDefaults = true
        ignoreUnknownKeys = true
    }

    override fun serialize(encoder: Encoder, value: Outgoing) {
        val array = arrayOf(json.encodeToString(value)) //this one should use default serializer. not this KSerializer
        val jsonString = json.encodeToString(array)
        encoder.encodeString(jsonString)
    }

    override fun deserialize(decoder: Decoder): Outgoing = Json.decodeFromString(decoder.decodeString())
}
I'm using it like this:
@Serializable(with = DDPMessageSerializer::class)
sealed class Outgoing(val msg: String) {
    @Serializable(with = DDPMessageSerializer::class) data class Connect(val session: String? = null, val version: String, val support: List<String>): Outgoing("connect")
}
webSocketSession?.sendSerialized(connectMessage)
This gives me StackOverflowError, cuz it always tries to serialize with this implementation
j

Johann Pardanaud

03/25/2023, 9:12 AM
Why are you using your custom serializer on the Outgoing class? 🤔
o

Osman Saral

03/25/2023, 9:23 AM
I'm trying to serialize it and it's subclasses
j

Johann Pardanaud

03/25/2023, 9:47 AM
Can’t you use the default serializer on the Outgoing class and the custom one only on the Connect class? I thought that was what you wanted.
o

Osman Saral

03/27/2023, 9:44 AM
I get this error when I do:
Multiple sealed subclasses of 'class com.example.ddpclient.Outgoing' have the same serial name 'DDPMessageSerializer': 'class com.example.ddpclient.Outgoing$Connect', 'class com.example.ddpclient.Outgoing$Method'
@Serializable
sealed class Outgoing(val msg: String) {
    @Serializable(with = DDPMessageSerializer::class) data class Connect(val session: String? = null, val version: String, val support: List<String>): Outgoing("connect")
    @Serializable(with = DDPMessageSerializer::class) data class Method(val method: String, val params: List<@Contextual Any>, val id: String?): Outgoing("method")
}
j

Johann Pardanaud

03/27/2023, 10:10 AM
OK, I did not see the sealed class. Could you give me multiple examples of the final JSON payload you want to generate? I’m not sure anymore and it could help us find a solution to your issue.
o

Osman Saral

03/27/2023, 10:11 AM
I think I was able to write a WebSocketContentConverter class:
class DDPMessageConverter(
    private val json: Json,
): WebsocketContentConverter {
    override suspend fun serializeNullable(
        charset: Charset,
        typeInfo: TypeInfo,
        value: Any?
    ): Frame {
        if (value !is Outgoing) return Frame.Text("")
        val message = encodeToDDPMessage(value)

        return Frame.Text(message)
    }

    override suspend fun deserialize(charset: Charset, typeInfo: TypeInfo, content: Frame): Any? {
        TODO("Not yet implemented")
    }

    override fun isApplicable(frame: Frame): Boolean {
        return frame is Frame.Text
    }


    private fun encodeToDDPMessage(message: Outgoing): String {
        val array = arrayOf(json.encodeToString(message))
        return json.encodeToString(array)
    }
}
here is the sample result
["{\"type\":\"com.example.ddpclient.Outgoing.Connect\",\"msg\":\"connect\",\"version\":\"1\",\"support\":[\"1\"]}"]