Reputation: 91
Currently, I use retrofit2 to call restful apis and get response. Because the response body can be multiple types, I wrote the code following.
//Interface
@FormUrlEncoded
@POST("payments/events/{id}")
fun postPayment(@Path("id") id: String): Call<Any>
//Api Manager
fun postPayment(id: String): Observable<Any> {
return Observable.create {
subscriber ->
val callResponse = api.postPayment(id)
val response = callResponse.execute()
if (response.isSuccessful) {
if (response.body() is MyClass1) {
// never success...
} else if (response.body() is MyClass2) {
// never success...
}
subscriber.onNext(response.body())
subscriber.onCompleted()
} else {
subscriber.onError(Throwable(response.message()))
}
}
}
So I'm not able to cast response.body()
to MyClass1
or MyClass2
.
response.body() as MyClass1
occurs error too.
MyClass1
and MyClass2
are normal template classes.
class MyClass1( val id: String, val data: String)
Is there any smart way to cast response body to my custom classes?
Small update for MyClass2
class MyClass2( val token: String, val url: String, val quantity: Int)
Upvotes: 2
Views: 6171
Reputation: 91
First of all, thanks @Bryan for answer. Your answer was perfect but finally I did something tricky way.
...
if (response.isSuccessful) {
val jsonObject = JSONObject(response.body() as Map<*, *>)
val jsonString = jsonObject.toString()
if (jsonObject.has("id")) {
val myclass1Object = Gson().fromJson(jsonString, MyClass1::class.java)
...
} else {
val myclass2Object = Gson().fromJson(jsonString, MyClass2::class.java)
...
}
}
...
Upvotes: 1
Reputation: 15155
As mentioned by @Miha_x64, Retrofit doesn't know about your classes (MyClass1
and MyClass2
) because your Call
uses the Any
type. Therefore, Retrofit is not creating an instance of MyClass1
or MyClass2
, instead it is just creating an instance of the Any
class.
The simplest solution would just be to combine the two classes:
data class MyClass(
val id: String?,
val data: String?,
val token: String?,
val url: String?,
val quantity: Int
)
Then you can specify the response type in your interface:
@FormUrlEncoded
@POST("payments/events/{id}")
fun postPayment(@Path("id") id: String): Call<MyClass>
In the case your response does not have an id
or data
element, they will just be null
. Then you can check which type of response was received simply by checking which values are null
:
if (response.body().id != null) {
// Handle type 1 response...
} else if (response.body().token != null) {
// Handle type 2 response...
}
A slightly more complex solution would be to write a wrapper for your two classes, and a type adapter to populate the wrapper. This would avoid the nullability of each of the fields, as well as keep your data structure separated.
This would differ based on the ConverterFactory
you are using but if, for example, you are using Gson, it would look something like this:
data class ApiResponse(
val val1: MyClass1? = null,
val val2: MyClass2? = null
)
class ApiResponseAdapter : TypeAdapter<ApiResponse> {
@Throws(IOException::class)
override fun write(out: JsonWriter, value: ApiResponse?) {
if (value != null) {
out.beginObject()
value.val1?.id? let { out.name("id").value(it) }
value.val1?.data? let { out.name("data").value(it) }
value.val2?.token? let { out.name("token").value(it) }
value.val2?.url? let { out.name("url").value(it) }
value.val2?.quantity? let { out.name("quantity").value(it) }
out.endObject()
} else {
out.nullValue()
}
}
@Throws(IOException::class)
override fun read(in: JsonReader): ApiResponse {
reader.beginObject()
var id: String? = null
var data: String? = null
var token: String? = null
var url: String? = null
var quantity: Int = 0
while(in.hasNext()) {
val name = in.nextName()
if (name.equals("id", true)) {
id = in.nextString()
} else if (name.equals("data", true)) {
data = in.nextString()
} else if (name.equals("token", true)) {
token = in.nextString()
} else if (name.equals("url", true)) {
url = in.nextString()
} else if (name.equals("quantity", true)) {
quantity = in.nextInt()
}
}
reader.endObject()
if (id != null && data != null) {
return ApiResponse(MyClass1(id, data), null)
} else if (token != null && url != null) {
return ApiResponse(null, MyClass2(token, url, quantity))
} else {
return ApiResponse()
}
}
}
Then you can add this type adapter to your Gson instance:
val gson = GsonBuilder().registerTypeAdapter(ApiResponse::class.java, ApiResponseAdapter()).create()
Then replace the Call<Any>
type with Call<ApiRepsone>
and you can then check which response was received by checking which value is null
:
if (response.body().val1 != null) {
// Handle MyClass1 response...
} else if (response.body().val2 != null) {
// Handle MyClass2 response...
}
Upvotes: 4