Deserialization Polymorpism with Generic
I have to work with an API that responds with JSON:
- on Success
{ "Data":T } - on Error
{ "error":{ "msg":String, "id":Int } }
so i have built the corresponding classes.
@Serializable
abstract class SimpleResponse<out T>
@Serializable
data class Error<out T>(
val message: String,
val code: Int
) : SimpleResponse<T>()
@Serializable
data class Success<out T>(
val data: T
) : SimpleResponse<T>()
the generic in Success can be something like this:
@Serializable
data class InitResponse(
val location:Location,
val rights:Rights,
)
i defined the SerializationModule like this:
module= SerializersModule {
polymorphic(Any::class){
subclass(InitResponse::class)
}
polymorphic(SimpleResponse::class) {
subclass(Success.serializer(PolymorphicSerializer(InitResponse::class)))
}
}
But its throwing me
kotlinx.serialization.json.internal.JsonDecodingException: Polymorphic serializer was not found for missing class discriminator ('null')
Did i forgot to define something?
Can you please add the Json you're trying to deserialize? It looks like it does not have 'type' field
It's not having the type field, thats why I give him the type with the function call.
@POST("/")
suspend fun initDevice(@Body body:SimpleRequest):SimpleResponse<InitResponse>
Ok, now i added
class ResultSerializer<T>(private val dataSerializer: KSerializer<T>): KSerializer<Success<T>>{
override val descriptor: SerialDescriptor=dataSerializer.descriptor
override fun deserialize(decoder: Decoder): Success<T> = Success(dataSerializer.deserialize(decoder))
override fun serialize(encoder: Encoder, value: Success<T>) =dataSerializer.serialize(encoder,value.data)
}
but im stuck with the same bug as shown in this issue ^^
Hey @sandwwraith, I'm working with @Mitti30 on this problem in our app. As he described, we have two cases:
- success case: the request was handeled successful and returns json in the following format. The content of
Datacan change depending on the request type, therefore we useSuccessto wrap the actual type likeInitResponse. So for each API endpoints, we have a response typeSuccess<ResponseType>:
{
"Data":{
"someID":0,
"anotherProperty":[
"valueA",
"valueB"
]
}
}
- error case: something went wrong with the request, e.g. wrong username/password, resource not found or anything like that. In this case the server returns json like this:
{
"error":{
"message":"Username or password invalid",
"level":"DEBUG",
"code":401,
}
}
We now want to define a deserializer that can be used by retrofit to either return the Success<T> or Error object. As the json comes directly from our server, there is no type field provided. So we can not infer the type by looking at the json. Instead, it is only known from the type of the retrofit call as @Mitti30 showed above. I hope it is a bit easier to understand now.
I see the problem. The good approach for it is to remove polymorphic serialization and write your own custom serializer, like in this comment: https://github.com/Kotlin/kotlinx.serialization/issues/1313#issuecomment-770994374 . However, due to the mentioned kapt issue, it's impossible for now. Probably, a viable workaround can be to move models and serializers to the separate Gradle module, which is not processed by kapt.
Current workaround is to use JsonContentPolymorphicSerializer.
Also related: #1531
Current workaround is to use
JsonContentPolymorphicSerializer.
Would you mind pointing us to an example? Because when I tried to use it in the example below, I just get https://github.com/Kotlin/kotlinx.serialization/issues/1263 (?) ("No value passed for parameter 'typeSerial0'") on both .serializer() calls, and a type mismatch when constructing JsonContentPolymorphicSerializer<T>, as afaik, you cannot specify a generic type for a KClass (https://github.com/Kotlin/kotlinx.serialization/issues/2555).
@Serializable
sealed interface GenericResponse<T>
@Serializable
data class ErrorResponse<T>(
val message: String,
@SerialName("error_code")
val errorCode: String,
) : GenericResponse<T>
@Serializable
data class SuccessResponse<T>(
val message: SuccessMessage<T>,
) : GenericResponse<T>
@Serializable
data class SuccessMessage<T>(
val type: String,
val service: String,
val version: String,
val result: T,
)
class GenericResponseSerializer<T> : JsonContentPolymorphicSerializer<GenericResponse<T>>(GenericResponse::class) {
override fun selectDeserializer(element: JsonElement): KSerializer<out GenericResponse<T>> =
when {
"error_code" in element.jsonObject -> ErrorResponse.serializer()
else -> SuccessResponse.serializer()
}
}