Retrofit中如何处理同一字段返回不同数据类型的问题(OtpVerifyModel场景)
Hey there, let's tackle this tricky API response issue you're facing—having a single data field that switches between an object and a string depending on the result code is definitely a pain, but we've got solid fixes for it! Here are a few approaches, ordered from quick-and-dirty to type-safe and maintainable:
1. Quick Temporary Fix: Use Any for the data Field
If you need a fast solution without too much refactoring, you can change the data field's type to Any? and handle casting manually after receiving the response.
First, update your data class:
data class OtpVerifyModel( val result: Int? = null, val msg: String? = null, val data: Any? = null // Switch to Any type )
Then, when processing the Retrofit response:
val response = apiService.verifyOtp(...) if (response.isSuccessful) { response.body()?.let { model -> when (model.result) { 1 -> { // Safely cast to Datas object val successData = model.data as? Datas successData?.let { // Handle your successful data logic here } } -1 -> { // Safely cast to error string val errorMessage = model.data as? String errorMessage?.let { // Handle error message display/logic here } } } } }
Pros: Fast to implement, minimal code changes.
Cons: Not type-safe—risk of ClassCastException if the API returns unexpected types, and IDE won't help with autocompletion for the data field.
2. Type-Safe Best Practice: Custom Gson TypeAdapter + Sealed Class
This is the most elegant and maintainable solution for Kotlin, leveraging sealed classes to represent the two possible states of data, plus a custom Gson adapter to handle automatic parsing.
Step 1: Define a Sealed Class for data
Sealed classes let you represent restricted class hierarchies, perfect for cases where data can only be one of two types:
sealed class OtpData { data class Success(val datas: Datas) : OtpData() data class Error(val message: String) : OtpData() object Empty : OtpData() // Handle null/unexpected cases }
Step 2: Update Your Main Data Class
Modify OtpVerifyModel to use the sealed class:
data class OtpVerifyModel( val result: Int? = null, val msg: String? = null, val data: OtpData? = null )
Step 3: Create a Custom Gson TypeAdapter
This adapter tells Gson how to parse the data field based on its JSON type:
class OtpDataAdapter : TypeAdapter<OtpData>() { private val gson = Gson() private val datasType = object : TypeToken<Datas>() {}.type override fun write(out: JsonWriter, value: OtpData?) { // We only care about deserialization here—skip serialization for now throw UnsupportedOperationException("Serialization not implemented") } override fun read(`in`: JsonReader): OtpData { return when (`in`.peek()) { JsonToken.STRING -> { // Parse as error message string OtpData.Error(`in`.nextString()) } JsonToken.BEGIN_OBJECT -> { // Parse as Datas object OtpData.Success(gson.fromJson(`in`, datasType)) } JsonToken.NULL -> { `in`.nextNull() OtpData.Empty } else -> { // Skip unexpected types to avoid crashes `in`.skipValue() OtpData.Empty } } } }
Step 4: Register the Adapter with Retrofit
When building your Retrofit instance, add the custom adapter to Gson:
val customGson = GsonBuilder() .registerTypeAdapter(OtpData::class.java, OtpDataAdapter()) .create() val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) // Replace with your base URL .addConverterFactory(GsonConverterFactory.create(customGson)) .build()
Step 5: Handle the Response Safely
Now you can use Kotlin's when expression with full type safety:
val response = apiService.verifyOtp(...) if (response.isSuccessful) { response.body()?.let { model -> when (model.data) { is OtpData.Success -> { val successDatas = model.data.datas // Handle successful data logic } is OtpData.Error -> { val errorMsg = model.data.message // Handle error message logic } OtpData.Empty -> { // Handle null or unexpected data cases } } } }
Pros: 100% type-safe, no casting needed, IDE autocompletion works, and code is clean and maintainable.
Cons: Requires a bit more setup upfront, but it's worth it for long-term code health.
3. Alternative: Parse Response Body Manually
If you don't want to mess with custom adapters, you can have Retrofit return a raw ResponseBody and parse it yourself:
First, update your Retrofit interface:
@FormUrlEncoded @POST("Api/login") suspend fun verifyOtp( @Query("height") height: String, @Query("width") width: String, @Field("key") key: String, @Field("type") type: String, @Field("phone") phone: String, @Field("email") email: String, @Field("otp") otp: String, @Field("country_code") country_code: String, @Field("country_flag") country_flag: String ): Response<ResponseBody>
Then parse the response manually:
val response = apiService.verifyOtp(...) if (response.isSuccessful) { response.body()?.string()?.let { jsonString -> val jsonObj = JSONObject(jsonString) val result = jsonObj.getInt("result") if (result == 1) { val successModel = Gson().fromJson(jsonString, OtpSuccessModel::class.java) // Handle successModel.data (Datas type) } else if (result == -1) { val errorModel = Gson().fromJson(jsonString, OtpErrorModel::class.java) // Handle errorModel.data (String type) } } }
Where OtpSuccessModel and OtpErrorModel are separate data classes:
data class OtpSuccessModel( val result: Int? = null, val msg: String? = null, val data: Datas? = null ) data class OtpErrorModel( val result: Int? = null, val msg: String? = null, val data: String? = null )
Pros: No custom adapters needed.
Cons: More boilerplate code, and you lose the convenience of Retrofit's automatic parsing.
内容的提问来源于stack exchange,提问作者Rahul Pathak

