Kotlin中Retrofit多类型响应的解析崩溃处理方案
Great question—this is a super common gotcha when dealing with APIs that don’t stick to a single response format. The crash happens because Retrofit tries to parse the response strictly against the Call<List<Type>> you defined, but gets a JSON object instead of an array. Here are two solid ways to fix this in Kotlin:
Option 1: Manually Parse ResponseBody (Quick & Simple)
The easiest approach is to first fetch the raw ResponseBody, then manually check the response format and parse accordingly. This is perfect for one-off cases where you don’t want to overhaul your Retrofit setup.
Step 1: Update Your Interface
Change the return type to Call<ResponseBody> so you can access the raw JSON string:
@POST("user/userTimetable") fun getTypeApi( @HeaderMap headers: Map<String, String>, @Body body: ScheduleBody? ): Call<ResponseBody>
Step 2: Parse the Response in Callback
In your enqueue callback, check if the response is an array or object, then parse each case:
val gson = Gson() val call = apiService.getTypeApi(headers, body) call.enqueue(object : Callback<ResponseBody> { override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) { if (response.isSuccessful) { response.body()?.string()?.let { jsonString -> // First, check if the JSON is an array val jsonElement = gson.fromJson(jsonString, JsonElement::class.java) if (jsonElement.isJsonArray) { // Parse as your list of Type objects val typeList = gson.fromJson(jsonString, object : TypeToken<List<Type>>() {}.type) handleSuccess(typeList) } else if (jsonElement.isJsonObject) { // Parse as the error response val errorResponse = gson.fromJson(jsonString, ErrorResponse::class.java) handleError(errorResponse.message) } else { handleError("Invalid response format") } } ?: handleError("Empty response body") } else { handleError("Request failed with code: ${response.code()}") } } override fun onFailure(call: Call<ResponseBody>, t: Throwable) { handleError(t.message ?: "Unknown network error") } }) // Define your helper data classes data class Type(val type: String, val color: String) data class ErrorResponse(val message: String) // Example handler functions fun handleSuccess(data: List<Type>) { /* Process your list */ } fun handleError(message: String) { /* Show error to user */ }
Option 2: Sealed Class + Custom Gson Converter (Clean & Scalable)
For a more maintainable solution (especially if multiple APIs have this behavior), use a sealed class to represent both success and error responses, then create a custom Gson deserializer to handle the parsing automatically.
Step 1: Define a Sealed Class for Responses
This class will encapsulate both possible response types:
sealed class ApiResult { data class Success(val data: List<Type>) : ApiResult() data class Error(val message: String) : ApiResult() }
Step 2: Create a Custom JsonDeserializer
This deserializer will check the response type and map it to the appropriate sealed class variant:
class ApiResultDeserializer : JsonDeserializer<ApiResult> { override fun deserialize( json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext? ): ApiResult { json ?: return ApiResult.Error("Empty response") return when { json.isJsonArray -> { val typeList = context?.deserialize<List<Type>>( json, object : TypeToken<List<Type>>() {}.type ) ?: emptyList() ApiResult.Success(typeList) } json.isJsonObject -> { val errorResponse = context?.deserialize<ErrorResponse>(json, ErrorResponse::class.java) ApiResult.Error(errorResponse?.message ?: "Unknown error") } else -> ApiResult.Error("Invalid response format") } } }
Step 3: Configure Retrofit with the Custom Converter
Add the deserializer to your Gson instance when building Retrofit:
val customGson = GsonBuilder() .registerTypeAdapter(ApiResult::class.java, ApiResultDeserializer()) .create() val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create(customGson)) .build()
Step 4: Update Your Interface & Use the Sealed Class
Now your interface can return Call<ApiResult>, and you can handle the response cleanly with a when expression:
@POST("user/userTimetable") fun getTypeApi( @HeaderMap headers: Map<String, String>, @Body body: ScheduleBody? ): Call<ApiResult> // Usage val call = apiService.getTypeApi(headers, body) call.enqueue(object : Callback<ApiResult> { override fun onResponse(call: Call<ApiResult>, response: Response<ApiResult>) { if (response.isSuccessful) { when (val result = response.body()) { is ApiResult.Success -> handleSuccess(result.data) is ApiResult.Error -> handleError(result.message) } } else { handleError("Request failed with code: ${response.code()}") } } override fun onFailure(call: Call<ApiResult>, t: Throwable) { handleError(t.message ?: "Unknown network error") } })
Key Notes
- Avoid using
try-catchfor parsing checks if possible—checkingisJsonArray()/isJsonObject()is more efficient and intentional. - The sealed class approach keeps your code clean and makes it easy to extend if you need to handle more response types later.
内容的提问来源于stack exchange,提问作者Ammar Ahsan

