https://kotlinlang.org logo
#ktor
Title
# ktor
m

Martin Gaens

07/03/2022, 12:02 PM
I'm building a Telegram Bot API library in Ktor and I realized there's something I don't understand about Ktor client. I'm using
kotlinx.serialization
for serializing objects and here's my issue: The Telegram API supports a lot of methods. Here's an example of one such method: The
getFile
method. It has one parameter:
file_id
which is of type
String
. These methods can receive their parameters through JSON objects. So, if I want to send a request to this method, I first create a
class
called
GetFileJSONRequest
with
@SerialName()
annotations (because in Kotlin, the parameter is called
fileId
and in JSON it has to be
file_id
). And then, I send the request like this:
Copy code
val response: HttpResponse = <http://client.post|client.post>("<https://api.telegram.org/bot123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11/getFile|https://api.telegram.org/bot123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11/getFile>") {
    contentType(ContentType.Application.Json)
    setBody(GetFileJSONRequest(fileId = fileId))
}
This whole process with creating classes seems very tedious and I wonder if there's a much better solution to my problem.
a

Andreas Scheja

07/03/2022, 3:52 PM
you could just do a
setBody(mapOf("file_id" to fileId))
instead
m

Martin Gaens

07/03/2022, 5:28 PM
Oh yeah... Maps! Thank you!
I've got a problem with this solution though:
Serializing collections of different element types is not yet supported. Selected serializers: [kotlin.String, <http://kotlin.Int|kotlin.Int>]
. This is the code leading to the error:
Copy code
<http://client.post|client.post>("sendMessage") {
    contentType(ContentType.Application.Json)
    setBody(
        mapOf(
            "chat_id" to chatId.toString(),
            "text" to text,
            "parse_mode" to parseMode,
            "entities" to entities?.map { it.toDto() },
            "disable_web_page_preview" to disableWebPagePreview,
            "disable_notification" to disableNotification,
            "protect_content" to protectContent,
            "reply_to_message_id" to replyToMessageId,
            "allow_sending_without_reply" to allowSendingWithoutReply,
            "reply_markup" to replyMarkup
        )
    )
}
It is very unfortunate, but this is as clean as I could get my code for the
sendMessage
method to be:
Copy code
internal suspend fun sendMessage(
        chatId: Int,
        text: String,
        parseMode: String? = null,
        entities: List<MessageEntity>? = null,
        disableWebPagePreview: Boolean? = null,
        disableNotification: Boolean? = null,
        protectContent: Boolean? = null,
        replyToMessageId: Int? = null,
        allowSendingWithoutReply: Boolean? = null,
        replyMarkup: ReplyMarkup? = null
    ) {
        @Serializable
        class Request(
            @SerialName("chat_id") val chatId: Int,
            @SerialName("text") val text: String,
            @SerialName("parse_mode") val parseMode: String? = null,
            @SerialName("entities") val entities: List<MessageEntityDto>? = null,
            @SerialName("disable_web_page_preview") val disableWebPagePreview: Boolean? = null,
            @SerialName("disable_notification") val disableNotification: Boolean? = null,
            @SerialName("protect_content") val protectContent: Boolean? = null,
            @SerialName("reply_to_message_id") val replyToMessageId: Int? = null,
            @SerialName("allow_sending_without_reply") val allowSendingWithoutReply: Boolean? = null,
            @SerialName("reply_markup") val replyMarkup: ReplyMarkup? = null
        )
        <http://client.post|client.post>("sendMessage") {
            contentType(ContentType.Application.Json)
            setBody(Request(
                chatId = chatId,
                text = text,
                parseMode = parseMode,
                entities = entities?.map { it.toDto() },
                disableWebPagePreview = disableWebPagePreview,
                disableNotification = disableNotification,
                protectContent = protectContent,
                replyToMessageId = replyToMessageId,
                allowSendingWithoutReply = allowSendingWithoutReply,
                replyMarkup = replyMarkup
            ))
        }
    }
}
a

Aleksei Tirman [JB]

07/05/2022, 9:09 AM
To solve your problem you can use JSON objects, arrays and primitives:
Copy code
<http://client.post|client.post>("<https://httpbin.org/post>") {
    contentType(ContentType.Application.Json)
    setBody(JsonObject(mapOf(
        "fileId" to JsonPrimitive(123),
        "text" to JsonPrimitive("text"),
        "flag" to JsonPrimitive(true)
    )))
}
m

Martin Gaens

07/05/2022, 2:41 PM
There's still a problem: I have a lot of
@Serializable
classes. How do I pass objects of these classes into the map?
a

Aleksei Tirman [JB]

07/05/2022, 3:31 PM
Unfortunately, you can’t mix JsonObject, JsonPrimitive, etc with serializable classes.
m

Martin Gaens

07/05/2022, 3:52 PM
So what are my options?
a

Andreas Scheja

07/05/2022, 4:09 PM
If you're only targeting the JVM you could try Jackson instead of
kotlinx.serialization
, shouldn't have any problems with mixed maps afaik.
m

Martin Gaens

07/05/2022, 4:10 PM
Yeah okay I can but that's kinda sad cause I really like
kotlinx.serialization
. Does this mean it is not yet production ready?
a

Andreas Scheja

07/05/2022, 4:14 PM
I wouldn't say it's not production ready, just your specific use case (or rather how you prefer to use it) isn't covered yet.
m

Martin Gaens

07/05/2022, 4:24 PM
But am I not falling for the XY problem now? Are there any other ways to solve this? I just want to send a JSON object containing serializable objects to an API. That doesn't seem like something that should be impossible to achieve.
a

Andreas Scheja

07/05/2022, 5:24 PM
Tried around a bit more, there is
Json.encodeToJsonElement()
which will turn any serializable thing into a
JsonElement
which you then can pass to the JsonObject.
Copy code
@kotlinx.serialization.Serializable
    class MyObj(val myProperty: Double)
    runBlocking {
        val response = <http://client.post|client.post>("<http://localhost:8080/some/url>") {
            contentType(ContentType.Application.Json)
            setBody(
                JsonObject(
                    mapOf(
                        "file_id" to JsonPrimitive(123),
                        "name" to JsonPrimitive("test"),
                        "obj" to DefaultJson.encodeToJsonElement(MyObj(2.0))
                    )
                )
            )
        }
        println(response.status)
    }
m

Martin Gaens

07/05/2022, 5:29 PM
Awesome, thank you very much, I'll give it a try in the following hour
5 Views