如何使用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层的配置,也是性能最优的实现方式。
实现逻辑
- 先定义通用的统一响应类:
// 泛型响应包装,T为对应接口的DTO类型 data class ApiResponse<T>( val code: Int = 200, // *裸DTO场景下默认填充成功状态码* val message: String = "success", // *裸DTO场景下默认填充成功提示* val data: T? = null ) - 编写Moshi的自定义JsonAdapter工厂,专门处理
ApiResponse类型的反序列化:- 反序列化时先对根JSON节点做结构探测,注意一定要使用Moshi的peek操作,不要消费原始JSON流,否则后续正式解析时会因为流位置偏移出现错误
- 如果探测到根节点存在约定的外层标识字段(比如
code/message/data),按照标准包装结构解析,将data字段内容映射为泛型T - 如果探测到根节点没有外层标识字段,说明是服务端裸返回的DTO,直接将整个根节点的JSON内容解析为泛型T,作为
ApiResponse的data值,外层字段使用默认的成功值填充
- 将这个自定义适配器注册到Moshi实例中,和现有的Retrofit集成流程保持一致:
val moshi = Moshi.Builder() .add(ApiResponseAdapter.Factory()) // 注册自定义适配工厂 .build() val retrofit = Retrofit.Builder() .baseUrl("你的服务端地址") .addConverterFactory(MoshiConverterFactory.create(moshi)) .build() - 调整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错误页)的场景,这个方案更灵活。
实现逻辑
- 自定义CallAdapter工厂,拦截所有接口的响应转换流程
- 拿到原始的Response对象后,先读取响应体的原始JSON文本,做结构判断:
- 如果是标准包装结构,直接用Moshi反序列化为
ApiResponse<T> - 如果是裸DTO结构,手动构造
ApiResponse<T>实例,将解析后的DTO值填入data字段
- 如果是标准包装结构,直接用Moshi反序列化为
- 所有异常场景(网络错误、非200状态码)也统一构造为带错误状态码的
ApiResponse实例,抛给上层统一处理
方案优势
- 可以覆盖JSON解析之外的全链路统一响应逻辑,上层业务不需要单独处理网络异常、HTTP错误码
方案劣势
- 需要手动处理响应体的流读取,处理不当会导致流被消费无法二次解析的问题
- 改动范围比方案1大,需要覆盖原来Retrofit对
suspend函数、Flow返回值的适配逻辑
为什么之前尝试的Root注解方案不适用
Moshi相关的Root类注解本质是固定路径的映射规则:只能指定“从JSON的某个固定key路径下提取内容映射为当前对象”,无法动态识别根节点的结构差异。这类注解的设计初衷是解决固定嵌套层级的映射问题,天生不支持动态结构判断——碰到两种返回结构混合的场景,固定路径的映射规则要么找不到外层key,要么会把裸DTO的字段当成外层字段解析,完全不符合动态适配的需求。
内容的提问来源于stack exchange,提问作者DennisVA
相关产品推荐
相关产品推荐

