JakeWharton/retrofit2-kotlinx-serialization-converter

Response with null field which is not included in data class field caused polymorphic error

ronjunevaldoz opened this issue · 3 comments

Using this example response https://gist.githubusercontent.com/ronjunevaldoz/934758f12a3b0a63be1bde73da20c3df/raw/210ad1695695a2abdf4c14057bff422ba970c1ee/response-jobs.json

Sample data class

@Serializable
data class ActualJob(
    val id: Int,
    val id_uuid: String,
    val title: String,
    val description: String?,
    val reference: String = "",
    @SerialName("order_number")
    val orderNumber: String?,
    val type : String,
    val billed_client : String?,
    val address: JobAddress?,
    val job_status : JobStatus?,
    val priority: Int = 1,
    val clients : List<Client>,
    val entity_permissions : EntityPermission
)

Using Retrofit2 service (Coroutine Flow) result as ResponseBody

@GET("api/v1/job/actual")
suspend fun getJobs(): ResponseBody // working

// Assume all data classes are serialized already 
val jsonString= repository.getJob().string()  or from response
val jobs = AppJson.decodeFromString<ActualJob>(jsonString)
// This will works fine

Using Retrofit2 service (Coroutine Flow) result as Flow list of ActualJob

@GET("api/v1/job/actual")
suspend fun getJobs(): Flow<List<ActualJob>> // With error
    
// sample implementation
repository.getJobs()
   .catch { e-> emit(e)}
   .collectLatest { jobs-> emit(jobs) }

Sample working converter setup

val AppJson = Json {
    ignoreUnknownKeys = true
    encodeDefaults = true // to include default value in encoding
    coerceInputValues = true
    classDiscriminator = "#class" // to avoid type as default reserve discriminator
}

builder .client(loggingClient) .addConverterFactory(AppJson.asConverterFactory(ContentType)) .build()

Dependencies (Kotlin Version: 1.5.31)

  implementation 'com.squareup.retrofit2:retrofit:2.9.0' 
  implementation 'com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0'

Error encountered

 kotlinx.serialization.json.internal.JsonDecodingException: Expected class kotlinx.serialization.json.JsonObject (Kotlin reflection is not available) as the serialized body of kotlinx.serialization.Polymorphic<Flow>, but had class kotlinx.serialization.json.JsonArray (Kotlin reflection is not available)
        at kotlinx.serialization.json.internal.JsonExceptionsKt.JsonDecodingException(JsonExceptions.kt:24)
        at kotlinx.serialization.json.internal.PolymorphicKt.decodeSerializableValuePolymorphic(Polymorphic.kt:91)
        at kotlinx.serialization.json.internal.StreamingJsonDecoder.decodeSerializableValue(StreamingJsonDecoder.kt:36)
        at kotlinx.serialization.json.Json.decodeFromString(Json.kt:100)
        at com.jakewharton.retrofit2.converter.kotlinx.serialization.Serializer$FromString.fromResponseBody(Serializer.kt:30)
        at com.jakewharton.retrofit2.converter.kotlinx.serialization.DeserializationStrategyConverter.convert(DeserializationStrategyConverter.kt:11)
        at com.jakewharton.retrofit2.converter.kotlinx.serialization.DeserializationStrategyConverter.convert(DeserializationStrategyConverter.kt:7)
        at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:243)
        at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:153)
        at okhttp3.RealCall$AsyncCall.run(RealCall.kt:138)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

Debug investigation

I believe decoding from string with loader causes the issue, i tried evaluating it with decodeFromString(string) it is working find, but decodeFromString(loader, string) the error was thrown.

  @OptIn(ExperimentalSerializationApi::class) // Experimental is only for subtypes.
  class FromString(override val format: StringFormat) : Serializer() {
    override fun <T> fromResponseBody(loader: DeserializationStrategy<T>, body: ResponseBody): T {
      val string = body.string()
      return format.decodeFromString(loader, string)
    }

    override fun <T> toRequestBody(contentType: MediaType, saver: SerializationStrategy<T>, value: T): RequestBody {
      val string = format.encodeToString(saver, value)
      return RequestBody.create(contentType, string)
    }
  }

Possible solution

I am thinking to use this sample answer https://stackoverflow.com/a/68466759/2801777

Not sure what this is but if I had to guess it's some behavior of kotlinx.serialization or your chosen serialization format and not specifically this library. Please provide a failing test case or reproducing sample that I can use to diagnose the problem.

Not sure what this is but if I had to guess it's some behavior of kotlinx.serialization or your chosen serialization format and not specifically this library. Please provide a failing test case or reproducing sample that I can use to diagnose the problem.

Hi, I believe this is a serializer issue, but when I try manual decoding and encoding the data class, from a response string. It is working fine. I have provided the investigation details above.

@JakeWharton

When I use "minifyEnabled true" that error message is thrown...

kotlinx.serialization.json.internal.JsonDecodingException: Polymorphic serializer was not found for missing class discriminator ('null') JSON input: {"success":true,"message":"Otp sent successfully","data":{"countryCode":"880","mobileNo":" 99999999","type":"New","testOtp":63401}}