Retrofit 为什么会收到带转义引号的 JSON 响应?
结论
响应示例 "{\"type\": ...}" 看起来是一个 JSON 字符串,字符串内部还包含 JSON 文本;它与直接返回 JSON 对象的 {"type": ...} 结构不同。如果内容确实来自服务器的原始响应,问题就发生在 Retrofit 转换之前:客户端收到的响应不是预期的 JSON 对象。不过,单看 Android Studio Network Inspector 的展示,无法确认原始字节内容,也无法判断差异来自服务端、请求条件还是中间缓存。
先检查原始响应
临时添加 OkHttp 日志拦截器,检查响应状态码、Content-Type、请求 URL 和响应体。日志拦截器应放在应用拦截器链中,并确保其他调试代码没有提前读取响应体。响应体只能消费一次;如果要手动检查,请使用 Response.peekBody(),不要先调用 response.body?.string(),否则 Retrofit 无法继续解析。
val logging = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(tokenInterceptor)
.addInterceptor(logging)
.build()
对比正常和异常请求的原始响应内容。如果响应以引号开头和结尾,且内部 JSON 的引号经过转义,说明它很可能是 JSON 字符串,而不是 JSON 对象。如果能看到实际的 \u0000 或 \0 字节,再记录它们出现的位置;不要仅凭 Inspector 的显示就断定响应中有空字符。
比较两次请求
在同一 Android 客户端中逐项比较请求 URL、查询参数、Authorization、Accept、Content-Type、重定向结果和缓存状态。你提供的两个请求路径使用了不同的时间参数,而异常只出现在其中一个时间点,因此需要确认异常是否与该时间参数对应的数据或服务端处理分支有关。
同时检查 Postman、Curl 和 Android 发出的请求是否完全一致。tokenInterceptor 会设置 Bearer token。如果服务端根据身份、请求头或缓存键返回不同内容,工具请求和应用请求就可能得到不同响应。目前的配置没有显示 Accept 请求头,建议先记录实际请求头,再判断是否需要显式设置。
调试时可以添加响应缓存控制,观察结果是否变化。如果要添加网络拦截器或其他缓存相关配置,先确认项目实际启用了 OkHttp 缓存,不要预先把缓存当成已证实的原因。
根据检查结果处理
- 如果原始响应确实是带引号的 JSON 字符串,应让接口返回预期的 JSON 对象,或确认 API 契约是否规定返回字符串。如果契约明确规定返回字符串,再将字符串内容作为第二步解析;不要直接全局删除反斜杠或引号。
- 如果原始响应是正常 JSON,但 Retrofit 解析失败,检查声明的响应类型是否与 JSON 结构一致,以及实际使用的 converter 是否支持该类型。
- 如果 Android 请求与 Postman、Curl 的请求不一致,先排除 URL、请求头、token、重定向或缓存差异,再复现问题。
- 如果请求一致但原始响应不同,保存两组脱敏后的请求与响应证据,并根据时间参数和响应头定位服务端处理差异。仅凭客户端配置无法确定具体是哪段服务端逻辑造成异常。
不要在生产日志中记录完整 Bearer token 或敏感响应数据。确认响应来源或数据契约后,再让 Retrofit 按预期结构解析。
备注:内容仅供参考。
