I am using ktor + kotlinx.serialization and i want to achieve a json response from call.respond(Respond("some mesage",null)) something like this :
when result = null
{ "message" : "some mesage" }
when result is any Type
{
"message" : "some mesage",
"result" : "showing result"
}
or
{
"message" : "some mesage",
"result" : 0.0
}
my Respond data class
#kotlinx.serialization.Serializable
data class Respond<T>(
#SerialName("message")
val message : String? = null,
#SerialName("result")
val result : T? = null
)
but it giving me an error like this :
Serializer for class 'Respond' is not found.
Mark the class as #Serializable or provide the serializer explicitly.
I achieved by create inheritance of BaseResponse then polymorphism Response that override type of result with nullable String :
#kotlinx.serialization.Serializable
sealed class BaseResponse<T> {
abstract val message: String?
abstract val result: T?
}
#kotlinx.serialization.Serializable
#SerialName("Response")
class Response(
#SerialName("message") override val message: String?, override val result: String? = null
) : BaseResponse<String>()
#kotlinx.serialization.Serializable
#SerialName("FullResponse")
class FullBaseResponse<T>(
#SerialName("message") override val message: String?, override val result: T
) : BaseResponse<T>()
val responseModule = SerializersModule {
polymorphic(BaseResponse::class) {
subclass(Response.serializer())
subclass(FullBaseResponse.serializer(PolymorphicSerializer(Any::class)))
}
}
and added json configuration :
install(ContentNegotiation) {
json(Json {
explicitNulls = false
serializersModule = responseModule
})
}
so now I just use call.respond(Respond("some message")) when I only want to show message
Related
First I'll post the code
#OptIn(ExperimentalSerializationApi::class)
#Serializer(forClass = UUID::class)
object UUIDserializer : KSerializer<UUID> {
override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString())
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: UUID) {
encoder.encodeString(value.toString())
}
}
typealias SID = #Serializable(with = UUIDserializer::class) UUID
fun randomSid() = UUID.randomUUID() as SID
#Serializable
data class Example(val id:SID = randomSid())
class SerializeId {
#Test
fun nestedTypeUsage() {
val example = Example()
val string = Json.encodeToString(example)
println(string)
}
#Test
fun directTypeUsage () {
val hi = randomSid()
val string = Json.encodeToString(hi)
println(string)
}
}
nestedTypeUsage run and passes, but directTypeUsage fails.
Serializer for class 'UUID' is not found.
Mark the class as #Serializable or provide the serializer explicitly.
kotlinx.serialization.SerializationException: Serializer for class 'UUID' is not found
I can't apply the #Serializable annotation directly to a val or a function parameter.
almost immediately after posting this. I realized I can
#Test
fun directTypeUsage () {
val hi = randomSid()
val string = hi.toString()
println(string)
}
Plugin and dependeny:
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
implementation "io.ktor:ktor-serialization:$ktor_version"
Application file:
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
install(ContentNegotiation) {
json()
}
userRouter()
}.start(wait = true)
}
UserRouter:
fun Application.userRouter() {
routing {
get("/users/{id}") {
val id = call.parameters["id"]?.toInt() ?: -1
val user = User("Sam", "sam#gmail.com", "abc123")
val response = if (id == 1) {
Response("hahaha", false)
} else {
Response(user, true) //< - here, use String type will work
}
call.respond(response)
}
}
}
User:
#Serializable
data class User(
val name: String,
val email: String,
val password: String
)
Response:
#Serializable
data class Response<T>(
val data: T,
val success: Boolean
)
Logs:
2021-12-02 18:04:34.214 [eventLoopGroupProxy-4-1] ERROR ktor.application - Unhandled: GET - /users/7
kotlinx.serialization.SerializationException: Serializer for class 'Response' is not found.
Mark the class as #Serializable or provide the serializer explicitly.
at kotlinx.serialization.internal.Platform_commonKt.serializerNotRegistered(Platform.common.kt:91)
at kotlinx.serialization.SerializersKt__SerializersKt.serializer(Serializers.kt:155)
Any help would be appreciated.
The problem is that type for the response variable is Response<out Any> (the lowest common denominator between String and User types is Any) and the serialization framework cannot serialize the Any type.
That's werid, the following works, but to me they are just the same:
get("/users/{id}") {
val id = call.parameters["id"]?.toInt() ?: -1
val user = User("Sam", "sam#gmail.com", "abc123")
/* val response = if (id == 1) { //<- determine response here won't work, why?
Response("hahaha", false)
} else {
Response(user, true)
}
call.respond(response)*/
if (id == 1) {
call.respond(Response("hahaha", false))
} else {
call.respond(Response(user, true))
}
}
You should give a type to a generic when you set a nullable generic to null, otherwise, the plugin doesn't know how to serialize it.
for eg.
//define
#Serializable
data class Response<T>(
val data: T?,
val success: Boolean
)
///usage:
val response = Response<String>(null, false)
With ktor client, I've got a non-serializable object derived from a serializable object like so:
#Serializable
#SerialName("login-request")
open class LoginRequest (
open val email : String = "",
open val password : String = ""
) : ServiceRequestPayload
impl class
class LoginRequestVo : LoginRequest("", ""), NodeComponent by NodeComponentImpl() {
override val email: String by subject("")
override val password: String by subject("")
}
Now when I manually use kotlinx serialization on this like so:
val request : LoginRequest = LoginRequestVo().apply {
email = "test#gmail.com"
password = "password"
}
val str = Json.encodeToString(request)
println(str)
It correctly serializes it, and when deserialized on the other side, it correctly deserializes to LoginRequest. However, when I use ktor-client to serialize my object, it complains that LoginRequestVo is not serializable. The example code below uses some other objects from my project and has more information that you need, but the gist is that the type of U in the invoke function and therefore the request.payload expression is type LoginRequest as specified by the LoginServiceImpl below.
suspend inline fun <T, U: ServiceRequestPayload> HttpClient.invoke(desc : EndpointDescriptor<T, U>, request : ServiceRequest<U>? ) : ServiceResponse {
val path = "/api${desc.path}"
return when(desc.method) {
HttpMethod.GET -> {
get(path)
}
HttpMethod.POST -> {
if (request == null) {
post(path)
} else {
post(path) {
contentType(ContentType.Application.Json)
body = request.payload
}
}
}
HttpMethod.DELETE -> delete(desc.path)
HttpMethod.PUT -> {
if (request == null) {
put(path)
} else {
post(path) {
contentType(ContentType.Application.Json)
body = request.payload
}
}
}
}
}
class LoginServiceImpl(
context : ApplicationContext
) : LoginService {
private val client by context.service<HttpClient>()
override suspend fun login(request: ServiceRequest<LoginRequest>) = client.invoke(LoginServiceDesc.login, request)
override suspend fun register(request: ServiceRequest<LoginRequest>) = client.invoke(LoginServiceDesc.register, request)
}
The error I get is:
My question is, Is there a way to specify to ktor-client the type or the serializer to use when it needs to serialize a body?
For example, I have JSON
{
"url": "//n.ya.com"
}
In order to deserialize, I define the data class
#Serializable
data class Foo(
#SerialName("url")
val link: String,
)
After deserializing the Foo object has
foo.link with "//n.ya.com"
How can I remove the // during the deserializing, which means foo.link with "n.ya.com"?
You can add custom Serializer for a single property:
#Serializable
data class Foo(
#SerialName("url")
#Serializable(with = FooLinkDeserializer::class)
val link: String,
)
object FooLinkSerializer : KSerializer<String> {
override val descriptor = PrimitiveSerialDescriptor("Foo.link", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): String {
return decoder.decodeString().substringAfter("//")
}
override fun serialize(encoder: Encoder, value: String) {
encoder.encodeString("//$value")
}
}
Or you can intercept JSON transformations using JsonTransformingSerializer:
#Serializable
data class Foo(
#SerialName("url")
#Serializable(with = FooLinkInterceptor::class)
val link: String,
)
object FooLinkInterceptor : JsonTransformingSerializer<String>(String.serializer()) {
override fun transformDeserialize(element: JsonElement): JsonElement {
return JsonPrimitive(element.jsonPrimitive.content.substringAfter("//"))
}
}
Let's say I'm having a class like:
#Serializable
data class MyClass(
#SerialName("a") val a: String?,
#SerialName("b") val b: String
)
Assume the a is null and b's value is "b value", then Json.stringify(MyClass.serializer(), this) produces:
{ "a": null, "b": "b value" }
Basically if a is null, I wanted to get this:
{ "b": "b value" }
From some research I found this is currently not doable out of the box with Kotlinx Serialization so I was trying to build a custom serializer to explicitly ignore null value. I followed the guide from here but couldn't make a correct one.
Can someone please shed my some light? Thanks.
You can use explicitNulls = false
example:
#OptIn(ExperimentalSerializationApi::class)
val format = Json { explicitNulls = false }
#Serializable
data class Project(
val name: String,
val language: String,
val version: String? = "1.3.0",
val website: String?,
)
fun main() {
val data = Project("kotlinx.serialization", "Kotlin", null, null)
val json = format.encodeToString(data)
println(json) // {"name":"kotlinx.serialization","language":"Kotlin"}
}
https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#explicit-nulls
Use encodeDefaults = false property in JsonConfiguration and it won't serialize nulls (or other optional values)
Try this (not tested, just based on adapting the example):
#Serializable
data class MyClass(val a: String?, val b: String) {
#Serializer(forClass = MyClass::class)
companion object : KSerializer<MyClass> {
override val descriptor: SerialDescriptor = object : SerialClassDescImpl("MyClass") {
init {
addElement("a")
addElement("b")
}
}
override fun serialize(encoder: Encoder, obj: MyClass) {
encoder.beginStructure(descriptor).run {
obj.a?.let { encodeStringElement(descriptor, 0, obj.a) }
encodeStringElement(descriptor, 1, obj.b)
endStructure(descriptor)
}
}
override fun deserialize(decoder: Decoder): MyClass {
var a: String? = null
var b = ""
decoder.beginStructure(descriptor).run {
loop# while (true) {
when (val i = decodeElementIndex(descriptor)) {
CompositeDecoder.READ_DONE -> break#loop
0 -> a = decodeStringElement(descriptor, i)
1 -> b = decodeStringElement(descriptor, i)
else -> throw SerializationException("Unknown index $i")
}
}
endStructure(descriptor)
}
return MyClass(a, b)
}
}
}
Since I was also struggling with this one let me share with you the solution I found that is per property and does not require to create serializer for the whole class.
class ExcludeIfNullSerializer : KSerializer<String?> {
override fun deserialize(decoder: Decoder): String {
return decoder.decodeString()
}
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("ExcludeNullString", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: String?) {
if (value != null) {
encoder.encodeString(value)
}
}
}
will work as expected with the following class
#Serializable
class TestDto(
#SerialName("someString")
val someString: String,
#SerialName("id")
#EncodeDefault(EncodeDefault.Mode.NEVER)
#Serializable(with = ExcludeIfNullSerializer::class)
val id: String? = null
)
Note the #EncodeDefault(EncodeDefault.Mode.NEVER) is crucial here in case you using JsonBuilder with encodeDefaults = true, as in this case the serialization library will still add the 'id' json key even if the value of id field is null unless using this annotation.
JsonConfiguration is deprecated in favor of Json {} builder since kotlinx.serialization 1.0.0-RC according to its changelog.
Now you have to code like this:
val json = Json { encodeDefaults = false }
val body = json.encodeToString(someSerializableObject)
As of now, for anyone seeing this pos today, default values are not serialized (see https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/basic-serialization.md#defaults-are-not-encoded-by-default)
So you simply add to set a default null value, and it will not be serialized.