kotlinx.serialization icon indicating copy to clipboard operation
kotlinx.serialization copied to clipboard

Deserialization Polymorpism with Generic

Open Mitti30 opened this issue 4 years ago • 7 comments

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?

Mitti30 avatar Jan 28 '21 15:01 Mitti30

Can you please add the Json you're trying to deserialize? It looks like it does not have 'type' field

sandwwraith avatar Jan 29 '21 13:01 sandwwraith

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>

Mitti30 avatar Jan 29 '21 13:01 Mitti30

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 ^^

Mitti30 avatar Feb 01 '21 16:02 Mitti30

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 Data can change depending on the request type, therefore we use Success to wrap the actual type like InitResponse. So for each API endpoints, we have a response type Success<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.

ln-12 avatar Feb 09 '21 16:02 ln-12

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.

sandwwraith avatar Feb 12 '21 17:02 sandwwraith

Current workaround is to use JsonContentPolymorphicSerializer.

Also related: #1531

sandwwraith avatar Dec 16 '24 18:12 sandwwraith

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()
        }
}

Kladki avatar Jan 06 '25 18:01 Kladki