简体   繁体   中英

Retrofit 2 response body to custom class

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)

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...
}

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)
        ...
    }
}
...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM