You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Retrofit+Gson登录报错:Expected BEGIN_OBJECT but was STRING求助

解决Retrofit登录时JSON解析异常问题(Expected BEGIN_OBJECT but was STRING)

错误原因

这个异常的核心问题是服务器返回的响应格式不统一:

  • 凭证错误时:返回标准JSON结构(包含message、errors字段)
  • 凭证正确时:直接返回纯字符串Token,而非你定义的LoginResponse对应的JSON对象

Gson转换器试图把纯字符串解析成LoginResponse对象,自然会抛出格式不匹配的异常。


方案一:改用ResponseBody手动处理响应(简单直接)

绕过Gson自动解析,手动处理响应体内容,适配两种返回格式:

  1. 修改API接口定义,将返回类型改为Call<ResponseBody>:
interface MyApi {
    @Headers("Accept:application/json")
    @POST(Constants.LOGIN_URL)
    fun login(@Body request : LoginRequest) : Call<ResponseBody>
}
  1. 调用接口时手动判断并解析响应:
apiService.login(loginRequest).enqueue(object : Callback<ResponseBody> {
    override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
        if (response.isSuccessful) {
            val responseContent = response.body()?.string() ?: ""
            // 根据内容开头判断是JSON还是纯字符串
            if (responseContent.startsWith("{")) {
                // 解析凭证错误的JSON响应
                val errorResp = Gson().fromJson(responseContent, LoginErrorResponse::class.java)
                // 处理错误逻辑,比如提示用户凭证错误
            } else {
                // 拿到纯字符串Token,按需处理
                val authToken = responseContent
                // 可手动构造LoginResponse对象,或直接使用Token
                val loginResp = LoginResponse(200, authToken, User("", "", "", "", ""))
                // 处理登录成功逻辑
            }
        } else {
            // 处理HTTP错误(如401、500等)
        }
    }

    override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
        // 处理请求失败逻辑
    }
})

新增错误响应数据类用于解析凭证错误场景:

data class LoginErrorResponse(
    @SerializedName("message") var message: String,
    @SerializedName("errors") var errors: Map<String, List<String>>
)

方案二:自定义Gson TypeAdapter自动适配两种格式

如果想保留Call<LoginResponse>的写法,可自定义TypeAdapter让Gson兼容字符串和JSON对象:

  1. 编写自定义TypeAdapter:
class LoginResponseTypeAdapter : TypeAdapter<LoginResponse>() {
    private val gson = Gson()

    override fun write(out: JsonWriter, value: LoginResponse?) {
        gson.toJson(value, LoginResponse::class.java, out)
    }

    override fun read(`in`: JsonReader): LoginResponse {
        return if (`in`.peek() == JsonToken.STRING) {
            // 纯字符串Token,构造LoginResponse对象(按需填充字段)
            val token = `in`.nextString()
            LoginResponse(200, token, User("", "", "", "", ""))
        } else {
            // 正常解析JSON对象
            gson.fromJson(`in`, LoginResponse::class.java)
        }
    }
}
  1. 在GsonBuilder中注册该TypeAdapter:
private val gson = GsonBuilder()
    .setLenient()
    .registerTypeAdapter(LoginResponse::class.java, LoginResponseTypeAdapter())
    .create()
  1. 接口保持原定义,正常调用即可:
apiService.login(loginRequest).enqueue(object : Callback<LoginResponse> {
    override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>) {
        if (response.isSuccessful) {
            val loginResp = response.body()
            // 处理登录成功逻辑,loginResp.authToken即为返回的Token字符串
        } else {
            // 解析错误响应
            val errorContent = response.errorBody()?.string() ?: ""
            val errorResp = Gson().fromJson(errorContent, LoginErrorResponse::class.java)
            // 处理错误逻辑
        }
    }

    override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
        // 处理请求失败逻辑
    }
})

额外建议

优先和后端沟通,统一返回格式(无论成功失败都返回JSON对象),这是最规范的做法,能避免客户端做额外适配。

内容的提问来源于stack exchange,提问作者Aj Catindig

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 05:48:20