json_annotation 用法与使用场景

json_annotation 用法与使用场景

本文整理 json_annotationjson_serializable 的常见用法。它们用于把 Dart 对象和 JSON 之间的转换交给代码生成,减少手写 fromJson / toJson 的错误。

1. json_annotation 是什么

json_annotation 提供 JSON 序列化相关注解,例如:

  • @JsonSerializable
  • @JsonKey
  • @JsonValue
  • JsonConverter

json_serializable 是代码生成器,读取这些注解并生成 .g.dart 文件。

两者关系:

json_annotation     提供注解
json_serializable   生成代码
build_runner        运行生成器

当前项目已经依赖:

dependencies:
  json_annotation: ^4.9.0

dev_dependencies:
  json_serializable: ^6.8.0
  build_runner: ^2.4.9

2. 基本模型写法

普通 Dart 类写法:

import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
  final String id;
  final String name;
  final int age;

  const User({
    required this.id,
    required this.name,
    required this.age,
  });

  factory User.fromJson(Map<String, dynamic> json) =>
      _$UserFromJson(json);

  Map<String, dynamic> toJson() => _$UserToJson(this);
}

运行:

dart run build_runner build --delete-conflicting-outputs

生成文件:

user.g.dart

使用:

final user = User.fromJson(json);
final map = user.toJson();

3. 与 Freezed 配合

当前项目主要是 Freezed + json_serializable 组合。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user_model.freezed.dart';
part 'user_model.g.dart';

@freezed
class UserModel with _$UserModel {
  const factory UserModel({
    required String id,
    required String username,
    required String passwordHash,
    required DateTime createdAt,
  }) = _UserModel;

  factory UserModel.fromJson(Map<String, dynamic> json) =>
      _$UserModelFromJson(json);
}

这里虽然没有显式写 @JsonSerializable(),但 Freezed 会帮生成类加上相应序列化配置。

使用:

final user = UserModel.fromJson(jsonDecode(raw));
final raw = jsonEncode(user.toJson());

当前项目示例:

await _prefs.setString(_kUserKey, jsonEncode(user.toJson()));
final user = UserModel.fromJson(jsonDecode(raw));

4. @JsonKey:字段名映射

后端字段名常用 snake_case,Dart 字段常用 camelCase。可以用 @JsonKey(name: ...) 映射。

@JsonSerializable()
class User {
  final String id;

  @JsonKey(name: 'user_name')
  final String username;

  @JsonKey(name: 'created_at')
  final DateTime createdAt;

  const User({
    required this.id,
    required this.username,
    required this.createdAt,
  });

  factory User.fromJson(Map<String, dynamic> json) =>
      _$UserFromJson(json);

  Map<String, dynamic> toJson() => _$UserToJson(this);
}

JSON:

{
  "id": "u_1",
  "user_name": "tom",
  "created_at": "2026-05-14T10:00:00.000Z"
}

适合:

  • API 字段是 snake_case。
  • 字段名需要兼容旧接口。
  • 后端字段名和 Dart 命名规范不一致。

5. @JsonKey:默认值

如果 JSON 缺少字段,可以用 defaultValue

@JsonKey(defaultValue: false)
final bool isAdmin;

完整示例:

@JsonSerializable()
class User {
  final String id;

  @JsonKey(defaultValue: false)
  final bool isAdmin;

  const User({
    required this.id,
    required this.isAdmin,
  });

  factory User.fromJson(Map<String, dynamic> json) =>
      _$UserFromJson(json);

  Map<String, dynamic> toJson() => _$UserToJson(this);
}

当 JSON 没有 isAdmin 时,结果为:

false

注意:如果你使用 Freezed,通常也会用 @Default

@Default(false) bool isAdmin

区别:

写法 作用
@Default(false) Freezed 构造对象时默认值
@JsonKey(defaultValue: false) JSON 缺字段时默认值

Freezed 中 @Default 通常会同时影响生成的 JSON 逻辑,但遇到复杂场景时仍可显式加 @JsonKey

6. @JsonKey:忽略字段

某些字段只在本地使用,不需要参与 JSON。

@JsonKey(includeFromJson: false, includeToJson: false)
final bool selected;

旧写法也常见:

@JsonKey(ignore: true)
final bool selected;

适合:

  • UI 选中状态。
  • 本地临时字段。
  • 缓存标记。
  • 不应提交给后端的字段。

示例:

@JsonSerializable()
class Device {
  final String id;
  final String name;

  @JsonKey(includeFromJson: false, includeToJson: false)
  final bool selected;

  const Device({
    required this.id,
    required this.name,
    this.selected = false,
  });
}

7. @JsonKey:自定义 fromJson / toJson

单个字段需要特殊转换时,可以指定函数。

@JsonKey(fromJson: _dateFromMs, toJson: _dateToMs)
final DateTime createdAt;

static DateTime _dateFromMs(int value) {
  return DateTime.fromMillisecondsSinceEpoch(value);
}

static int _dateToMs(DateTime value) {
  return value.millisecondsSinceEpoch;
}

完整示例:

@JsonSerializable()
class Event {
  final String id;

  @JsonKey(fromJson: _dateFromMs, toJson: _dateToMs)
  final DateTime createdAt;

  const Event({
    required this.id,
    required this.createdAt,
  });

  static DateTime _dateFromMs(int value) {
    return DateTime.fromMillisecondsSinceEpoch(value);
  }

  static int _dateToMs(DateTime value) {
    return value.millisecondsSinceEpoch;
  }

  factory Event.fromJson(Map<String, dynamic> json) =>
      _$EventFromJson(json);

  Map<String, dynamic> toJson() => _$EventToJson(this);
}

适合:

  • 时间戳。
  • 金额单位转换。
  • 字符串和枚举转换。
  • 后端特殊字段格式。

8. JsonConverter

如果同一种转换要复用,使用 JsonConverter

class DateTimeMsConverter implements JsonConverter<DateTime, int> {
  const DateTimeMsConverter();

  @override
  DateTime fromJson(int json) {
    return DateTime.fromMillisecondsSinceEpoch(json);
  }

  @override
  int toJson(DateTime object) {
    return object.millisecondsSinceEpoch;
  }
}

使用在字段上:

@JsonSerializable()
class Event {
  final String id;

  @DateTimeMsConverter()
  final DateTime createdAt;

  const Event({
    required this.id,
    required this.createdAt,
  });

  factory Event.fromJson(Map<String, dynamic> json) =>
      _$EventFromJson(json);

  Map<String, dynamic> toJson() => _$EventToJson(this);
}

也可以用在类上,影响类内所有匹配字段:

@DateTimeMsConverter()
@JsonSerializable()
class Event {
  final DateTime createdAt;
  final DateTime updatedAt;

  const Event({
    required this.createdAt,
    required this.updatedAt,
  });
}

适合:

  • 多个模型复用相同转换。
  • 时间戳统一转换。
  • 金额分 / 元转换。
  • 坐标、颜色、文件大小等特殊类型。

9. @JsonValue:枚举值映射

后端枚举值不一定等于 Dart 枚举名,可以使用 @JsonValue

enum DeviceStatus {
  @JsonValue('offline')
  offline,

  @JsonValue('connecting')
  connecting,

  @JsonValue('online')
  online,
}

JSON:

{
  "status": "online"
}

Dart:

DeviceStatus.online

适合:

  • 后端枚举是字符串。
  • 后端枚举是数字。
  • 枚举值需要兼容旧协议。

数字枚举:

enum OrderStatus {
  @JsonValue(0)
  pending,

  @JsonValue(1)
  paid,

  @JsonValue(2)
  cancelled,
}

10. unknownEnumValue

后端可能返回未知枚举值时,可以配置兜底。

enum DeviceStatus {
  offline,
  connecting,
  online,
  unknown,
}

@JsonSerializable()
class Device {
  @JsonKey(unknownEnumValue: DeviceStatus.unknown)
  final DeviceStatus status;

  const Device({required this.status});
}

适合:

  • 后端新增枚举值,但客户端还没升级。
  • 设备协议版本不一致。
  • 需要防止反序列化直接失败。

11. explicitToJson

嵌套对象默认可能不会自动调用内部对象的 toJson。建议嵌套模型使用 explicitToJson: true

@JsonSerializable(explicitToJson: true)
class User {
  final String id;
  final Profile profile;

  const User({
    required this.id,
    required this.profile,
  });

  factory User.fromJson(Map<String, dynamic> json) =>
      _$UserFromJson(json);

  Map<String, dynamic> toJson() => _$UserToJson(this);
}

适合:

  • 模型里包含另一个模型。
  • 模型里包含模型列表。
  • 需要稳定生成嵌套 JSON。

Freezed 中可以这样配置:

@Freezed()
class User with _$User {
  @JsonSerializable(explicitToJson: true)
  const factory User({
    required String id,
    required Profile profile,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) =>
      _$UserFromJson(json);
}

12. fieldRename

如果后端统一使用 snake_case,可以用 fieldRename

@JsonSerializable(fieldRename: FieldRename.snake)
class User {
  final String userName;
  final DateTime createdAt;

  const User({
    required this.userName,
    required this.createdAt,
  });
}

对应 JSON:

{
  "user_name": "tom",
  "created_at": "2026-05-14T10:00:00.000Z"
}

适合:

  • 后端字段全部 snake_case。
  • 不想每个字段都写 @JsonKey(name: ...)

常见选项:

FieldRename.none
FieldRename.snake
FieldRename.kebab
FieldRename.pascal

13. includeIfNull

控制 toJson 是否包含 null 字段。

@JsonSerializable(includeIfNull: false)
class UserUpdateRequest {
  final String? nickname;
  final String? avatarUrl;

  const UserUpdateRequest({
    this.nickname,
    this.avatarUrl,
  });
}

如果:

UserUpdateRequest(nickname: 'tom', avatarUrl: null).toJson()

结果:

{
  "nickname": "tom"
}

适合:

  • PATCH 请求。
  • 更新资料接口。
  • null 不应传给后端的场景。

也可以字段级配置:

@JsonKey(includeIfNull: false)
final String? avatarUrl;

14. checked

checked: true 会生成更严格的反序列化检查。

@JsonSerializable(checked: true)
class User {
  final String id;
  final int age;

  const User({
    required this.id,
    required this.age,
  });
}

适合:

  • 对接口数据质量要求高。
  • 想更早发现字段类型错误。
  • 调试后端返回异常。

代价:

  • 生成代码略复杂。
  • 运行时会做更多检查。

中小项目可以先不开启,关键接口再单独使用。

15. createFactory 和 createToJson

可以控制是否生成 fromJsontoJson

只需要序列化请求,不需要反序列化:

@JsonSerializable(createFactory: false)
class LoginRequest {
  final String username;
  final String password;

  const LoginRequest({
    required this.username,
    required this.password,
  });

  Map<String, dynamic> toJson() => _$LoginRequestToJson(this);
}

只需要反序列化响应,不需要序列化:

@JsonSerializable(createToJson: false)
class UserResponse {
  final String id;
  final String username;

  const UserResponse({
    required this.id,
    required this.username,
  });

  factory UserResponse.fromJson(Map<String, dynamic> json) =>
      _$UserResponseFromJson(json);
}

适合:

  • 请求 DTO。
  • 响应 DTO。
  • 减少不必要生成代码。

16. anyMap

默认 JSON Map 类型是 Map<String, dynamic>。如果输入可能是 Map<Object?, Object?>,可以使用 anyMap

@JsonSerializable(anyMap: true)
class Config {
  final String name;

  const Config({required this.name});
}

适合:

  • YAML 转换结果。
  • 第三方库返回非字符串 key 的 Map。
  • 老代码兼容。

普通 API JSON 不需要开启。

17. 泛型 genericArgumentFactories

泛型模型需要特殊处理。

@JsonSerializable(genericArgumentFactories: true)
class ApiResponse<T> {
  final int code;
  final String message;
  final T data;

  const ApiResponse({
    required this.code,
    required this.message,
    required this.data,
  });

  factory ApiResponse.fromJson(
    Map<String, dynamic> json,
    T Function(Object? json) fromJsonT,
  ) =>
      _$ApiResponseFromJson(json, fromJsonT);

  Map<String, dynamic> toJson(
    Object? Function(T value) toJsonT,
  ) =>
      _$ApiResponseToJson(this, toJsonT);
}

使用:

final response = ApiResponse<User>.fromJson(
  json,
  (value) => User.fromJson(value as Map<String, dynamic>),
);

适合:

  • 统一 API 响应。
  • 分页模型。
  • 通用 Result 包装。

如果项目规模不大,泛型 JSON 会增加复杂度,可以先写具体响应模型。

18. build.yaml 全局配置

如果全项目统一规则,可以写 build.yaml

示例:

targets:
  $default:
    builders:
      json_serializable:
        options:
          explicit_to_json: true
          field_rename: snake
          include_if_null: false

适合:

  • 后端统一 snake_case。
  • 全项目都不希望输出 null 字段。
  • 全项目都需要嵌套对象显式 toJson

注意:

  • 全局配置会影响所有模型。
  • 引入前要确认已有模型不会被破坏。
  • 老项目迁移时建议逐步启用。

19. 与 SQLite 转换的区别

json_annotation 面向 JSON,不直接等同于数据库行转换。

当前项目中 DeviceModel 同时有 JSON 和 SQLite 行转换:

factory DeviceModel.fromJson(Map<String, dynamic> json) =>
    _$DeviceModelFromJson(json);

SQLite 转换:

extension DeviceModelDb on DeviceModel {
  static DeviceModel fromRow(Map<String, dynamic> row) {
    return DeviceModel(
      id: row['id'] as String,
      name: row['name'] as String,
      bleAddress: row['ble_address'] as String,
      boundAt: DateTime.parse(row['bound_at'] as String),
    );
  }

  Map<String, dynamic> toRow() => {
        'id': id,
        'name': name,
        'ble_address': bleAddress,
        'bound_at': boundAt.toIso8601String(),
      };
}

为什么不直接复用 JSON:

  • 数据库字段可能是 snake_case。
  • 枚举存储可能用 .name
  • DateTime 存储格式可能需要控制。
  • 数据库列可能不是完整 JSON。

建议:

  • API JSON 使用 fromJson / toJson
  • SQLite 使用 fromRow / toRow

20. 与 Freezed 的常见组合

Freezed 数据类中常见写法:

@freezed
class DeviceModel with _$DeviceModel {
  const factory DeviceModel({
    required String id,
    required String name,
    @Default(DeviceStatus.offline) DeviceStatus status,
    required DateTime boundAt,
  }) = _DeviceModel;

  factory DeviceModel.fromJson(Map<String, dynamic> json) =>
      _$DeviceModelFromJson(json);
}

如果需要字段映射:

@freezed
class UserModel with _$UserModel {
  const factory UserModel({
    required String id,
    @JsonKey(name: 'user_name') required String username,
  }) = _UserModel;

  factory UserModel.fromJson(Map<String, dynamic> json) =>
      _$UserModelFromJson(json);
}

如果需要嵌套显式 toJson:

@freezed
class UserModel with _$UserModel {
  @JsonSerializable(explicitToJson: true)
  const factory UserModel({
    required String id,
    required Profile profile,
  }) = _UserModel;

  factory UserModel.fromJson(Map<String, dynamic> json) =>
      _$UserModelFromJson(json);
}

21. 什么时候适合使用 json_annotation

场景 是否推荐
API 请求模型 推荐
API 响应模型 推荐
本地 JSON 缓存 推荐
SharedPreferences 存对象 推荐
Freezed 数据模型 JSON 推荐
SQLite 行转换 可辅助,但通常手写 fromRow / toRow
临时 Map 拼接 不一定需要
简单一两个字段的一次性对象 可手写

22. 常见错误

22.1 忘记 part

part 'user.g.dart';

没有它会导致生成函数找不到。

22.2 忘记 fromJson / toJson

普通类需要手写入口:

factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);

Freezed 类通常只需要:

factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

22.3 忘记运行 build_runner

现象:

  • _$UserFromJson 找不到。
  • _$UserToJson 找不到。
  • .g.dart 不存在。

处理:

dart run build_runner build --delete-conflicting-outputs

22.4 JSON 字段类型和 Dart 类型不匹配

例如后端返回:

{
  "age": "18"
}

Dart 写:

final int age;

会导致转换失败。需要统一后端类型,或使用自定义转换:

@JsonKey(fromJson: _intFromJson)
final int age;

22.5 嵌套对象没有正确 toJson

如果嵌套对象输出不符合预期,检查是否需要:

@JsonSerializable(explicitToJson: true)

22.6 滥用 dynamic

不推荐:

final dynamic data;

推荐为接口定义明确类型。确实不确定时,可以用:

final Map<String, dynamic> data;

或为不同类型建联合模型。

23. 本项目实践建议

  • API / 本地 JSON 模型继续使用 Freezed + json_annotation
  • SharedPreferences 存对象时,使用 toJson + jsonEncode
  • 从 SharedPreferences 读取时,使用 jsonDecode + fromJson
  • SQLite 转换继续使用 fromRow / toRow
  • 如果后续接入后端 API 且字段为 snake_case,可以考虑 @JsonKey(name: ...)fieldRename
  • 如果后端时间字段是时间戳,使用 JsonConverter 统一转换。
  • 如果后端枚举可能扩展,使用 unknownEnumValue 做兜底。

推荐验证命令:

dart run build_runner build --delete-conflicting-outputs
dart format lib test
flutter analyze
flutter test

24. 参考资料

  • json_annotation 官方包:https://pub.dev/packages/json_annotation
  • json_serializable 官方包:https://pub.dev/packages/json_serializable
  • build_runner 官方包:https://pub.dev/packages/build_runner
posted @ 2026-05-18 10:51  呢哇哦比较  阅读(51)  评论(0)    收藏  举报