Hi, I'm trying to implement a DDP client using kto...
# serialization
o
Hi, I'm trying to implement a DDP client using ktor WebSocket. DDP messages needs to be serialized like this (when doing manually)
Copy code
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
would it help to make an extension function that encapsulates all of this functionality? like
fun FooKtorContext.sendOb(obj: MyDdpObj) = ...
o
ofcourse that would work but I wanted learn how it's done actually.
j
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
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
I don’t see any limitation, is this what you want?
Copy code
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:
Copy code
{"strings":"[\"hello\",\"world\"]"}
SomePayload(strings=[hello, world])
o
Here is my implementation:
Copy code
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:
Copy code
@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")
}
Copy code
webSocketSession?.sendSerialized(connectMessage)
This gives me StackOverflowError, cuz it always tries to serialize with this implementation
j
Why are you using your custom serializer on the Outgoing class? 🤔
o
I'm trying to serialize it and it's subclasses
j
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
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'
Copy code
@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
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
I think I was able to write a WebSocketContentConverter class:
Copy code
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\"]}"]