square / retrofit

A type-safe HTTP client for Android and the JVM
https://square.github.io/retrofit/
Apache License 2.0
43.01k stars 7.3k forks source link

Unwrapping envelopes using Kotlin Serialization? #4219

Open robpridham-bbc opened 3 weeks ago

robpridham-bbc commented 3 weeks ago

Hi. For some time now, we've been using Retrofit with Moshi, and we used a Converter to unwrap the usual 'envelope', like this:

@JsonClass(generateAdapter = true)
data class Envelope<T>(val data: T)

...

object EnvelopeConverter : Factory() {

    override fun responseBodyConverter(
        type: Type,
        annotations: Array<Annotation>,
        retrofit: Retrofit
    ): Converter<ResponseBody, *>? {

        val envelopedType = Types.newParameterizedType(Envelope::class.java, type)
        val delegate: Converter<ResponseBody, Envelope<Any>>? =
            retrofit.nextResponseBodyConverter(this, envelopedType, annotations)

        return Unwrapper(delegate)
    }

    private class Unwrapper<T>(
        private val delegate: Converter<ResponseBody, Envelope<T>>?
    ) : Converter<ResponseBody, T> {

        override fun convert(value: ResponseBody): T? {
            return delegate?.convert(value)?.data
        }
    }
}

I imagine you're familiar with this - a fairly common pattern to improve the usability of responses, and in fact a long time ago Jake shared a Gson version of the same thing in a presentation.

We have explored replicating this under Kotlin Serialization and we cannot determine a way forward. The direct equivalent seems to be something like:

@Keep
@Serializable
data class Envelope<T>(val data: T)

...

object EnvelopeConverter : Factory() {

    @OptIn(ExperimentalStdlibApi::class)
    override fun responseBodyConverter(
        type: Type,
        annotations: Array<Annotation>,
        retrofit: Retrofit
    ): Converter<ResponseBody, *>? {
        val envelopeContentsType = KTypeProjection.invariant(type::class.starProjectedType)
        val envelopedType = Envelope::class.createType(listOf(envelopeContentsType)).javaType
        val delegate: Converter<ResponseBody, Envelope<Any>>? =
            retrofit.nextResponseBodyConverter(this, envelopedType, annotations)

        return Unwrapper(delegate)
    }

    private class Unwrapper<T>(
        private val delegate: Converter<ResponseBody, Envelope<T>>?
    ) : Converter<ResponseBody, T> {

        override fun convert(value: ResponseBody): T? {
            return delegate?.convert(value)?.data
        }
    }
}

This fails because Envelope<Any> is (reasonably) not understood by Kotlin Serialization:

kotlinx.serialization.SerializationException: Serializer for class 'Any' is not found.

I might be missing something but I can't see that we can be more specific with the class when only supplied with type as a method parameter.

I should add that in our case, it is important that we configure Retrofit centrally and make it available to decentralised modules to parse their own data objects. We therefore cannot centrally define all the possible polymorphic types and have the resultant Kotlin Serialization polymorphic parser make sense of the type for us.

I think therefore this would fall to Retrofit to support. Have you got any opinions or advice please?