Retrofit+Gson解析API返回200状态码但response.body全为null
Retrofit+Gson 接口200但response.body()全字段为null排查方案
排查步骤(按问题出现概率从高到低排序)
最高优先级:检查OkHttp自定义拦截器的响应读取逻辑
如果你是自己实现拦截器打印响应日志,大概率是消费了响应流但没有重新构建响应传给下游:OkHttp的ResponseBody是单向流,内容只能被读取一次,如果你直接调用response.body()?.string()打印日志,读完流就会关闭,后续Retrofit的Gson转换器再读取时拿到的是空内容,解析出来自然全字段为null,且不会抛出明确异常。
修复方案:- 打印日志时改用
response.peekBody(Long.MAX_VALUE).string()读取内容,peekBody不会消费原始响应流,不影响后续解析流程; - 如果已经调用了
string()方法消费了流,必须重新构建Response对象,把读取到的字符串重新包装成ResponseBody塞回响应中再返回给链式调用的下一环。
官方提供的HttpLoggingInterceptor已经做了流复用处理,直接用官方依赖不会触发这个问题,自定义拦截器非常容易踩这个坑。
- 打印日志时改用
第二优先级:核对响应JSON结构和实体类层级、字段名的匹配度
这类问题占剩下场景的90%:- 先把拦截器打印的完整JSON复制出来,逐层级和你定义的
LoginResponse类比对:比如你定义的LoginResponse直接对应id、faunadb_token、data三个字段,但实际接口最外层多了一层通用包装(例如结构为{"code":200,"msg":"success","data":{"id":"xxx","faunadb_token":"xxx","user_detail":{...}}}),Gson匹配不到对应字段时会直接给字段赋值null,不会主动抛错。 - 字段名严格匹配:如果接口返回的是下划线命名(如
faunadb_token),实体类写字段名要么和JSON完全一致,要么给驼峰命名的字段加@SerializedName("对应JSON的key")注解,要么初始化Gson时配置setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)开启自动驼峰转下划线映射。注意拼写错误(比如少打字母、后缀写错)也会导致匹配失败。 - 字段类型要匹配:比如接口返回的
id是字符串类型,你实体类定义成Int/Long,Gson类型转换失败时也可能赋值null,建议给Gson加上setLenient()开启宽松模式,避免类型不匹配直接解析失败。
- 先把拦截器打印的完整JSON复制出来,逐层级和你定义的
第三优先级:检查Retrofit的Gson转换器配置是否正确
常见配置错误:- 没有添加
GsonConverterFactory,或者添加顺序错误:Retrofit默认只能将响应解析为ResponseBody类型,必须在构建Retrofit实例时调用addConverterFactory(GsonConverterFactory.create(你自定义配置的Gson实例)),如果在Gson转换器之前添加了ScalarsConverterFactory等其他转换器,可能导致响应被提前转换为字符串,无法正常序列化为实体类。
正确配置参考:
val customGson = GsonBuilder() .setLenient() // 接口下划线转驼峰才需要加这行,字段名完全匹配就不用 .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .create() val retrofit = Retrofit.Builder() .baseUrl(ApiConfig.BASE_URL) .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create(customGson)) .build()- ApiService接口定义的泛型错误:比如返回值写成
Call<Response<LoginResponse>>嵌套了两层Retrofit的Response,或者协程接口写错返回值类型,都会导致解析逻辑异常。
- 没有添加
第四优先级:检查Kotlin实体类和混淆规则
- 响应实体类不要写成其他类的非静态内部类(Kotlin里普通内部类默认持有外部类引用,Gson反射实例化时会失败),建议把所有响应实体类单独放在
model/response包下,定义为顶层类。 - 实体类字段建议给默认值,避免Gson反射赋值时因为构造函数参数问题失败,正确写法参考:
data class LoginResponse( @SerializedName("user_id") val userId: String = "", @SerializedName("faunadb_token") val faunadbToken: String = "", @SerializedName("data") val userInfo: UserDetail = UserDetail() ) data class UserDetail( val nickname: String = "", val avatar: String = "" )- 如果是release包出现该问题,debug包正常,优先检查混淆规则:需要在proguard配置中保留所有响应实体类的字段不被混淆,否则混淆后字段名变成a/b/c,无法和JSON key匹配。添加规则:
-keepattributes Signature -keepattributes *Annotation* -keep class 你自己的包名.model.** { <fields>; } -dontwarn sun.misc.**- 响应实体类不要写成其他类的非静态内部类(Kotlin里普通内部类默认持有外部类引用,Gson反射实例化时会失败),建议把所有响应实体类单独放在
快速定位技巧
如果以上步骤排查完还没找到问题,直接在拦截器中拿到响应字符串后,手动用Gson做一次解析,catch住异常打印日志,Gson会明确告诉你是哪个字段、哪一层级解析失败:
val responseStr = response.peekBody(Long.MAX_VALUE).string() try { val testParse = customGson.fromJson(responseStr, LoginResponse::class.java) Log.d("GSON_PARSE_TEST", "解析结果:$testParse") } catch (e: Exception) { Log.e("GSON_PARSE_TEST", "解析失败详情", e) }
内容的提问来源于stack exchange,提问作者provent
相关产品推荐
相关产品推荐

