关键词:Flutter、json_serializable、JsonKey、json_annotation、fromJson、toJson、build_runner、模型层
一、需求来源
「开发 Flutter 数据模型时遇到一个痛点:后端字段和 Dart 字段对不上,手写 fromJson 又臭又长,还总在 null 和新增枚举值上翻车。」
dart
factory User.fromJson(Map<String, dynamic> json) {
return User(
userName: json['user_name'] as String, // 后端是下划线命名
age: int.parse(json['age'].toString()), // 后端给的是字符串
avatar: json['avatar'] as String?, // 可能整个 key 都没有
);
}
Map<String, dynamic> toJson() => {
'user_name': userName,
'age': age.toString(),
'avatar': avatar, // 为 null 时还要不要发?
};
痛点有三:
- 字段名不一致得手写映射 ------ 后端
user_name、uid、create_time满天飞,字段一多就漏。 - 类型不一致得手写转换 ------ 时间戳、字符串数字、嵌套对象,每个模型重写一遍。
- 边界情况直接崩 ------ key 缺失、值为 null、后端新增枚举值,全是线上事故。
json_serializable 能消掉重复劳动,但生成器默认只认「字段名 == JSON key」。本项目模型就是典型:
dart
// lib/models/living_info_entity.dart:14
@JsonSerializable()
class LivingInfoEntity {
late int prepareId; // JSON 里就是 prepareId,零配置可用
late LivingInfoAnchor anchor;
factory LivingInfoEntity.fromJson(Map<String, dynamic> json) => $LivingInfoEntityFromJson(json);
Map<String, dynamic> toJson() => $LivingInfoEntityToJson(this);
}
字段名一旦对不上、类型对不上或需要容错,就必须请出 @JsonKey。
需求很朴素:把「字段名映射、取值位置、类型转换、缺省兜底」这些脏活,从手写代码改成声明式注解。
二、使用示例
2.1 name:字段名映射(最常用,推荐)
dart
@JsonSerializable()
class User {
@JsonKey(name: 'user_name')
final String? userName;
User({this.userName});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
JSON {"user_name": "Alex"} → Dart user.userName,生成代码等价于:
dart
userName: json['user_name'] as String?,
2.2 fromJson / toJson:类型转换
dart
DateTime parseDate(Object? value) => DateTime.parse(value.toString());
String dateToJson(DateTime value) => value.toUtc().toIso8601String();
@JsonKey(fromJson: parseDate, toJson: dateToJson)
final DateTime createdAt;
2.3 defaultValue + includeIfNull:缺省与空值
dart
@JsonKey(defaultValue: 0)
final int age; // JSON 里没有 age → 0
@JsonKey(includeIfNull: false)
final String? avatar; // avatar 为 null → toJson 时不带这个 key
2.4 综合示例:一个模型覆盖所有常见场景
dart
enum UserStatus { active, disabled, unknown }
Object? readUserId(Map map, String key) => map['user_id'] ?? map['userId'] ?? map['uid'];
@JsonSerializable()
class User {
/// 兼容后端多个字段名
@JsonKey(readValue: readUserId)
final String id;
/// JSON 字段名不同
@JsonKey(name: 'user_name')
final String? userName;
/// JSON → Dart 自定义转换
@JsonKey(fromJson: parseDate)
final DateTime createdAt;
/// Dart → JSON 自定义转换
@JsonKey(toJson: dateToJson)
final DateTime updatedAt;
/// null 时不发送
@JsonKey(includeIfNull: false)
final String? avatar;
/// 后端新增枚举值不崩
@JsonKey(unknownEnumValue: UserStatus.unknown)
final UserStatus status;
/// 只读取不回传
@JsonKey(includeToJson: false)
final String? serverToken;
/// 纯客户端字段,完全不参与序列化
@JsonKey(ignore: true)
final bool isSelected;
User({
required this.id,
this.userName,
required this.createdAt,
required this.updatedAt,
this.avatar,
required this.status,
this.serverToken,
this.isSelected = false,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
三、源码讲解
按职责分类,后面就不会混:
typescript
@JsonKey
├── 字段映射 name、readValue
├── JSON → Dart fromJson、includeFromJson、required、defaultValue、disallowNullValue、unknownEnumValue
├── Dart → JSON toJson、includeToJson、includeIfNull
└── 字段控制 ignore
3.1 name 改变字段名,readValue 改变取值位置
dart
@JsonKey(name: 'user_name')
final String? userName;
// 生成:userName: json['user_name'] as String?
readValue 可同时兼容多个后端字段名,用来兜住历史包袱:
dart
static Object? readUserId(Map map, String key) {
return map['user_id'] ?? map['userId'] ?? map['uid'];
}
@JsonKey(name: 'user_id', readValue: readUserId)
final String id;
易忽略的细节: readValue 第二个参数收到的是 name 的值('user_id'),不是 Dart 字段名。
3.2 readValue 和 fromJson 的分工
readValue 决定从哪拿,fromJson 决定拿到后怎么转。
dart
@JsonKey(readValue: readUserId, fromJson: parseInt)
final int userId;
scss
{"uid": "123"} → readUserId() → "123" → parseInt() → 123
3.3 required 和 disallowNullValue 不是一回事
两者的边界完全不同:
| 配置 | Key 不存在 | Key 存在但值为 null |
|---|---|---|
required: true |
抛异常 | 允许 |
disallowNullValue: true |
允许 | 抛异常 |
| 两者都为 true | 抛异常 | 抛异常 |
dart
@JsonKey(required: true)
final String id; // 协议里 id 必须存在
@JsonKey(disallowNullValue: true)
final String? name; // name 可以不出现,但出现了就不能是 null
3.4 defaultValue 只兜底「key 缺失」,不管非法值
dart
@JsonKey(defaultValue: 0)
final int age;
它只解决 {} 这种情况。指望它把 "abc" 变成 0 是想多了 ,类型不对照样抛异常,那得靠 fromJson。
3.5 unknownEnumValue:服务端加枚举值不再崩客户端
长期维护的 App API 里最有价值的属性:
dart
enum UserStatus { active, disabled, unknown }
@JsonKey(unknownEnumValue: UserStatus.unknown)
final UserStatus status;
后端返回 "deleted" 而客户端未发版时,默认行为是解析失败、整个接口炸;加了兜底只会退化成 UserStatus.unknown。
关键坑: 用了 unknownEnumValue 就别再自己写 fromJson 处理枚举,容易把兜底覆盖掉。
3.6 ignore / includeFromJson / includeToJson:三个方向的开关
| 配置 | JSON → Dart | Dart → JSON |
|---|---|---|
| 默认 | ✅ | ✅ |
ignore: true |
❌ | ❌ |
includeFromJson: false |
❌ | ✅ |
includeToJson: false |
✅ | ❌ |
dart
@JsonKey(ignore: true)
final String? localToken; // 双向都忽略:本地缓存、UI 状态
@JsonKey(includeToJson: false)
final String? serverGenerated; // 只读不回传:服务端生成的字段
易忽略的细节: ignore 是双向的,includeFromJson / includeToJson 是单向的。想做「只读」用 includeToJson: false,写成 ignore 就连读都读不到了。
3.7 includeIfNull:只影响 toJson
dart
@JsonKey(includeIfNull: false)
final String? nickname;
值为 null 时生成 {} 而非 {"nickname": null}。只作用于序列化输出,不影响反序列化,最适合用在请求参数上。
四、总结
@JsonKey 做的事很朴素:把「一个 Dart 字段如何与 JSON 字段对应」的全部规则,声明式地写在字段上。
核心价值:
- name / readValue 解决字段对不上:命名风格差异、后端多套字段名,一个注解搞定。
- fromJson / toJson 解决类型对不上:时间、数字字符串、嵌套结构,转换逻辑就在字段旁边。
- required / disallowNullValue / defaultValue 划清空值边界:key 缺失和值为 null 是两件事,别混着配。
- unknownEnumValue 是长期维护的保险丝:后端新增枚举值不该让老版本客户端崩溃。
- ignore / includeFromJson / includeToJson 控制数据流向:区分「双向忽略」和「单向只读」,能避开一半的序列化怪问题。
判断口诀:「我是在改字段名、改取值位置、改类型转换,还是改字段是否参与序列化?」
注解不是银弹,但它把模型层从「跟着后端字段跑」变成了「按约定描述映射」------ 这才是模型层该有的样子。