3. 接口响应数据模型设计:数据类定义、字段映射、@SerializedName、嵌套对象
3.1 数据类在 Retrofit 中的角色
Retrofit 通过转换器(Converter)将 HTTP 响应体 JSON 自动反序列化为 Kotlin 数据类实例。数据类定义了响应中每个字段的类型、名称和默认值,是整个网络层的数据契约。设计不当将直接导致解析失败、字段丢失、空指针或运行时异常。
| 角色 | 说明 |
|---|---|
| 类型安全契约 | 编译期确定字段类型,Gson/Moshi 按此映射 JSON 值 |
| 自动序列化/反序列化 | 无需手动解析 JSONObject/JSONArray |
| 嵌套结构表达 | 数据类内部可嵌套其他数据类,对应 JSON 对象层级 |
| 空安全与默认值 | Kotlin 的可空类型与默认值处理服务端字段缺失 |
3.2 字段映射原理与 @SerializedName
默认情况下,Gson 将 JSON 的 key 与数据类的属性名按字符完全匹配。但服务端接口常用下划线命名法(snake_case),而 Kotlin 代码习惯 驼峰命名法(camelCase)。@SerializedName 注解用于建立两者之间的映射关系。
// 服务端返回示例
{
"user_id": 1001,
"nick_name": "张三",
"avatar_url": "https://example.com/avatar.png"
}
// Kotlin 数据类定义
data class UserInfo(
@SerializedName("user_id")
val userId: Int,
@SerializedName("nick_name")
val nickName: String,
@SerializedName("avatar_url")
val avatarUrl: String?
)
3.3 嵌套对象与层级建模
服务端返回的 JSON 通常包含多层嵌套,数据类必须逐层对应。
// JSON 结构
{
"code": 200,
"message": "success",
"data": {
"id": 42,
"profile": {
"real_name": "李四",
"age": 28
}
}
}
// Kotlin 数据类嵌套定义
data class ApiResponse<T>(
val code: Int,
val message: String,
val data: T?
)
data class UserDetail(
val id: Long,
val profile: Profile
)
data class Profile(
@SerializedName("real_name")
val realName: String,
val age: Int
)
⚠️ 注意:嵌套深度建议控制在 3 层以内,过深增加解析开销且难以维护。对于通用分页结构,可提取为独立数据类复用。
3.4 字段映射流程(内联 SVG)
3.5 常见错误与避坑指南
| 错误模式 | 现象 | 解决方案 |
|---|---|---|
| 字段名不匹配 | 对应字段为 null 或默认值 | 使用 @SerializedName 明确映射服务端 key |
| 声明为非空类型但 JSON 缺失 | 反序列化抛出 NullPointerException | 使用可空类型 String? 并设置默认值 |
| 嵌套对象类型不匹配 | 解析失败,data 为 null | 嵌套必须定义为内部 data class,且字段类型精确 |
| 泛型擦除导致转换异常 | 运行时类型丢失 | 使用 object : TypeToken<ApiResponse<UserData>>() {}.type 显式传递类型 |
字段名含关键字 (如 class) | 编译错误 | 使用 @SerializedName 并改属性名,如 clazz |
3.6 调试技巧:日志验证字段映射
在开发阶段,通过 OkHttp 日志拦截器打印完整响应体,再与数据类期望结构逐一比对。
// 在 OkHttpClient 配置中添加日志拦截器
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
// 日志输出示例
{
"user_id": 1001,
"nick_name": "张三",
"avatar_url": null
}
// 对应数据类字段梳理
userId ← "user_id" ✓
nickName ← "nick_name" ✓
avatarUrl ← "avatar_url" ? 允许 null
💡 调试建议:在 Android Studio 的 Debug 视图下展开 Retrofit 返回的 Response.body(),查看每个字段的值是否与预期一致。若发现字段为 null,优先检查 @SerializedName 拼写和 JSON key 大小写。
3.7 性能与稳定性优化
- 按需定义字段:只声明业务需要的字段,忽略无用字段可减少解析时间与内存占用。
- 避免过深嵌套:嵌套深度 > 3 层时考虑使用
@JsonAdapter自定义反序列化,或扁平化结构。 - 使用 @SerializedName 减少冗余:对频繁出现的长 key 映射为短属性名,提升代码可读性。
- 统一基类处理通用字段:将
code、message等通用字段提取到BaseResponse,使用泛型传递data。
// 通用基类示例
open class BaseResponse(
val code: Int = -1,
val message: String = ""
)
data class UserListResponse(
val data: List<UserInfo>? = null
) : BaseResponse()
3.8 完整示例:多嵌套数据模型
// 服务端原始响应
{
"status": 0,
"data": {
"list": [
{
"product_id": "P001",
"product_name": "智能手表",
"price_info": {
"current": 299.00,
"original": 399.00,
"currency": "CNY"
}
}
],
"page": {
"total": 1,
"page_size": 20,
"current_page": 1
}
}
}
// Kotlin 数据模型
data class ProductListResponse(
val status: Int,
val data: ProductListData?
)
data class ProductListData(
val list: List<Product>,
val page: PageInfo
)
data class Product(
@SerializedName("product_id")
val productId: String,
@SerializedName("product_name")
val productName: String,
@SerializedName("price_info")
val priceInfo: PriceInfo
)
data class PriceInfo(
val current: Double,
val original: Double,
val currency: String
)
data class PageInfo(
val total: Int,
@SerializedName("page_size")
val pageSize: Int,
@SerializedName("current_page")
val currentPage: Int
)
⚠️ 特别注意:嵌套数据类中的字段如果预期服务端一定返回,则使用非空类型;若可能缺失(如价格历史字段偶发),必须使用可空类型
Double? 或设置默认值 0.0,避免整体解析失败。