androidjsonkotlinktor

Using Ktor, how do I deserialize JSON responses with lists of different types?


I am working on an Android app that uses Ktor for HTTP requests to an API. Depending upon the query, the response may contain a list of one of two types. The code works when I define four data classes: One each for the response types and two different result data classes -- one for each result type. However, I would like to know if it's possible to use only one response data class and tell it which data class to use for the list when I make the HTTP request.

I'm using the Kotlin serialization libaray

The classes look like the following:

`

@Serializable
data class ResultOne(
    val count: Int,
    val result: List<TypeOne>
)

@Serializable
data class ResultTwo(
    val count: Int,
    val result: List<Type2>
)

@Serializable
data class TypeOne(
   val str1: String,
   val str2: String,
   val int1: Int
)

@Serializable
data class TypeTwo(
    val str1: String,
    val dbl1: Double,
    val int1: Int
)

And the request looks something like this:


...
import io.ktor.serialization.kotlinx.json.json
...


private val httpClient = HttpClient {    
    install(ContentNegotiation) {
        json(Json{
            isLenient = true
            ignoreUnknownKeys = true
        })
    }
}


fun getResultTypeOne(){
    val response = responseOne(listOf(1, 2)).result
}


private suspend fun responseOne(ints: List<Int>): ResultOne {
    val availableShows = httpClient.get("https://my.api.com/search")
    return availableShows.body()
}

fun getResultTypeTwo(){
    val response = responseTwo(listOf(1, 2)).result
}


private suspend fun responseTwo(ints: List<Int>): ResultTwo {
    val availableShows = httpClient.get("https://my.api.com/search")
    return availableShows.body()
}

There's a bit of redundancy in the HTTP calls, as they are the same with the exception of specifying a different data class for the difference result types. Is there a way to instead reuse the same HTTP call, but tell it which result type to expect in that call?


Solution

  • Of course, it is possible. You should replace your ResultOne and ResultTwo classes with one generic class:

    @Serializable
    data class Result <T> (
       val count: Int,
       val result: T
    )
    

    This way, you can reuse your Result class for both TypeOne and TypeTwo, and your HTTP requests can be simplified:

    private val httpClient = HttpClient {    
        install(ContentNegotiation) {
            json(Json{
                isLenient = true
                ignoreUnknownKeys = true
            })
        }
    }
    
    
    fun getResultOne(){
        val response = response<TypeOne>(listOf(1, 2)).result
    }
    
    fun getResultTwo(){
        val response = response<TypeTwo>(listOf(1, 2)).result
    }
    
    private suspend fun <T> response(ints: List<Int>): Result<T> {
        val availableShows = httpClient.get("https://my.api.com/search")
        return availableShows.body()
    }
    

    If you have other questions, feel free to ask.