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

如何使用Moshi与Retrofit将接口返回DTO包装为统一Response对象

统一响应包装的Moshi+Retrofit实现路径

核心需求是解决服务端两种返回结构的兼容问题:

  • 结构A(裸DTO):JSON根节点就是目标DTO字段(比如示例里的{"email":"test@gmail.com","name":"test"})
  • 结构B(带外层包装):JSON根节点包含公共字段(code、message、data),目标DTO在data字段内部
    目标是不管碰到哪种结构,都能自动映射到统一的ApiResponse<T>对象,不需要针对单个接口写特殊解析逻辑。

方案1:自定义Moshi动态结构适配JsonAdapter(首选方案)

这个方案完全基于Moshi的原生扩展能力实现,改动量最小,不需要侵入Retrofit层的配置,也是性能最优的实现方式。

实现逻辑

  1. 先定义通用的统一响应类:
    // 泛型响应包装,T为对应接口的DTO类型
    data class ApiResponse<T>(
        val code: Int = 200, // *裸DTO场景下默认填充成功状态码*
        val message: String = "success", // *裸DTO场景下默认填充成功提示*
        val data: T? = null
    )
    
  2. 编写Moshi的自定义JsonAdapter工厂,专门处理ApiResponse类型的反序列化:
    • 反序列化时先对根JSON节点做结构探测,注意一定要使用Moshi的peek操作,不要消费原始JSON流,否则后续正式解析时会因为流位置偏移出现错误
    • 如果探测到根节点存在约定的外层标识字段(比如code/message/data),按照标准包装结构解析,将data字段内容映射为泛型T
    • 如果探测到根节点没有外层标识字段,说明是服务端裸返回的DTO,直接将整个根节点的JSON内容解析为泛型T,作为ApiResponse的data值,外层字段使用默认的成功值填充
  3. 将这个自定义适配器注册到Moshi实例中,和现有的Retrofit集成流程保持一致:
    val moshi = Moshi.Builder()
        .add(ApiResponseAdapter.Factory()) // 注册自定义适配工厂
        .build()
    
    val retrofit = Retrofit.Builder()
        .baseUrl("你的服务端地址")
        .addConverterFactory(MoshiConverterFactory.create(moshi))
        .build()
    
  4. 调整Retrofit接口定义,所有接口统一返回ApiResponse<对应DTO>类型,比如原来的登录接口调整为:
    @POST("login")
    suspend fun login(@Body request: LoginRequest): ApiResponse<UserDTO>
    

方案优势

  • 完全在Moshi解析流程内部完成结构判断,不会产生重复解析JSON的性能开销
  • 不需要对现有Retrofit、Moshi的核心配置做侵入式修改
  • 可以灵活调整外层结构的判断规则,比如匹配哪些字段属于统一外层的标识,不需要改动业务代码

方案2:自定义Retrofit CallAdapter.Factory

如果除了JSON结构适配,还需要统一处理HTTP状态码、网络异常、错误响应(比如500返回的HTML错误页)的场景,这个方案更灵活。

实现逻辑

  1. 自定义CallAdapter工厂,拦截所有接口的响应转换流程
  2. 拿到原始的Response对象后,先读取响应体的原始JSON文本,做结构判断:
    • 如果是标准包装结构,直接用Moshi反序列化为ApiResponse<T>
    • 如果是裸DTO结构,手动构造ApiResponse<T>实例,将解析后的DTO值填入data字段
  3. 所有异常场景(网络错误、非200状态码)也统一构造为带错误状态码的ApiResponse实例,抛给上层统一处理

方案优势

  • 可以覆盖JSON解析之外的全链路统一响应逻辑,上层业务不需要单独处理网络异常、HTTP错误码

方案劣势

  • 需要手动处理响应体的流读取,处理不当会导致流被消费无法二次解析的问题
  • 改动范围比方案1大,需要覆盖原来Retrofit对suspend函数、Flow返回值的适配逻辑

为什么之前尝试的Root注解方案不适用

Moshi相关的Root类注解本质是固定路径的映射规则:只能指定“从JSON的某个固定key路径下提取内容映射为当前对象”,无法动态识别根节点的结构差异。这类注解的设计初衷是解决固定嵌套层级的映射问题,天生不支持动态结构判断——碰到两种返回结构混合的场景,固定路径的映射规则要么找不到外层key,要么会把裸DTO的字段当成外层字段解析,完全不符合动态适配的需求。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.11.02 00:24:01