Seb
Seb

Reputation: 3215

How to handle Array of data in retrofit response

I am building an app need to pull some data from 2 endpoints: /api/products and /api/type

The first endpoint return a JSON and this is working fine. However, the second endpoint does not return a json but an Array of object.

The response sent back by api/type looks like this :

[["Model",123],["ModelB",456],["ModelC",789]]

when the other endpoints return the "usual" json.

I am using retrofit, okhttp and dagger-hilt.

The retrofit module is setup as below:

@InstallIn(SingletonComponent::class)
@Module
class APIModule {

    @Singleton
    @Provides
    @Named("default")
    fun provideDefaultOkHttpClient(): OkHttpClient =
        OkHttpClient
            .Builder()
            .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
            .build()



    @Singleton
    @Provides
    fun provideRetrofit(
        @Named("default") okHttpClient: OkHttpClient
    ): Retrofit = Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
        .baseUrl("https://test.com")
        .client(okHttpClient)
        .build()
}

This is working fine when calling api/products but I got an error below when using api/type:

java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 3 path $[0]

The Api service is as below:

interface Service {

    @GET("api/type")
    suspend fun getType(): Array<Pair<String, Int>>

I understand that the issue is that for one endpoint the response is a JSON and in the other case, it's an array like: [["Model",123],["ModelB",456],["ModelC",789]]

Any idea how to make it works ? Also is there any addConverterFactory that allow me to support both json and array at the same time ?

Upvotes: 0

Views: 45

Answers (1)

dmortal
dmortal

Reputation: 599

Create a class to be used in the data structure:

@JsonAdapter(DataDeserializer::class)
internal data class Data(
    val string: String,
    val value: Int,
)

Write custom deserializer:

internal class DataDeserializer : JsonDeserializer<Data> {
    override fun deserialize(
        json: JsonElement?,
        typeOfT: Type?,
        context: JsonDeserializationContext?
    ): Data {
        val jsonArray = requireNotNull(json?.asJsonArray)
        val string = jsonArray[0].asString
        val value = jsonArray[1].asInt
        return Data(string, value)
    }
}

The service will look like this:

internal interface Service {
    @GET("api")
    suspend fun getData(): Array<Data>
}

Be sure to specify addConverterFactory(GsonConverterFactory.create()) and you can substitute test data for the server by adding an interceptor:

@Singleton
@Provides
fun provideDefaultOkHttpClient(): OkHttpClient =
    OkHttpClient
        .Builder()
        .addInterceptor { chain ->
            Response.Builder()
                .request(chain.request())
                .code(200)
                .protocol(Protocol.HTTP_2)
                .message("message")
                .body("[[\"Model\",123],[\"ModelB\",456],[\"ModelC\",789]]".toResponseBody("application/json".toMediaType()))
                .build()
        }
        .build()

@Singleton
@Provides
fun provideRetrofit(
    okHttpClient: OkHttpClient
): Retrofit = Retrofit.Builder()
    .addConverterFactory(GsonConverterFactory.create())
    .baseUrl("https://test.com")
    .client(okHttpClient)
    .build()

Upvotes: 1

Related Questions