将 JSON 转换为具有空安全的 Dart 类对于健壮的 Flutter 应用至关重要。使用我们的免费在线工具可即时生成,或在生产代码库中使用 json_serializable 包。始终区分 required 字段、可选字段(?) 和可为 null 的字段(Type?)。freezed 包添加了不可变性和 copyWith()。使用注解后需运行 `flutter pub run build_runner build`。
- Dart 空安全要求用 ? 标记可为 null 的字段,并对必填构造函数参数使用 required 关键字。
- json_serializable + build_runner 消除了 fromJson/toJson 样板代码——用 @JsonSerializable() 注解并运行代码生成。
- freezed 在 json_serializable 基础上创建不可变数据类,具有 copyWith()、== 重写和模式匹配功能。
- 嵌套对象需要自己的 fromJson 工厂方法;对类型化列表使用 List<T>.from(json['key'].map((x) => T.fromJson(x)))。
- DateTime 解析在 fromJson 中使用 DateTime.parse(),在 toJson 中使用 .toIso8601String()。
- 对于枚举,使用 @JsonKey(unknownEnumValue: EnumType.unknown) 处理枚举定义中尚未包含的值。
- 始终编写往返单元测试:expect(User.fromJson(user.toJson()), equals(user)) 以尽早发现序列化错误。
为什么要将 JSON 转换为 Dart 类?
Flutter 应用使用 JSON 与 REST API、WebSocket 和本地存储进行通信。没有强类型的 Dart 类,你只能使用动态 Map——没有自动补全、没有编译时检查,运行时异常随时可能发生。Dart 的健全空安全使这一点更加重要:编译器强制正确处理可为 null 的值。
从 JSON 生成 Dart 模型类带来以下好处:
- 编译时类型安全:访问不存在的属性,分析器会立即捕获。
- IDE 自动补全:每个字段、方法和嵌套对象都可通过 IntelliSense 发现。
- 空安全强制执行:Dart 在语言级别将可为 null (String?) 与不可为 null (String) 分离。
- 重构信心:重命名类中的字段,分析器会标记所有需要更新的使用位置。
- 可测试性:不可变值对象易于构建、比较和在单元测试中断言。
Dart 类剖析:fromJson 和 toJson
规范的 Dart 模型类有三个部分:字段(带空安全注解)、用于反序列化的工厂构造函数以及用于序列化的方法:
class User {
final int id;
final String name;
final String? email; // 可为 null
final DateTime createdAt;
const User({
required this.id,
required this.name,
this.email,
required this.createdAt,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String?,
createdAt: DateTime.parse(json['created_at'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'created_at': createdAt.toIso8601String(),
};
}
}该类的关键设计决策:
final字段使类在构造后实际上不可变。required在构造函数中强制调用者始终提供非 null 字段。- 工厂构造函数将每个 JSON 值转换为其 Dart 类型,将 snake_case 键转换为 camelCase 字段。
String?(可为 null 的 String)映射到可以为 null 或缺失的 JSON 值。
生成类中的空安全
Dart 2.12+ 强制执行健全空安全。这意味着类型系统跟踪哪些值可以为 null,哪些不能。从 JSON 生成类时,必须正确识别可为 null 与不可为 null 的字段:
// 非 null:字段始终存在且不为 null
final String name; // json['name'] 始终是 String
// 可为 null:字段可能缺失或显式为 null
final String? bio; // json['bio'] 可以为 null
// 空断言:你确定运行时值不为 null
final String admin = json['role']!; // 若为 null 则抛出异常!
// 空合并:值为 null 时提供默认值
final int age = json['age'] ?? 0; // 缺失时默认为 0最佳实践:优先提供显式默认值,而不是使用空断言运算符(!)。断言可能在运行时抛出"对 null 值使用了空值检查运算符"异常。
factory UserProfile.fromJson(Map<String, dynamic> json) {
return UserProfile(
id: json['id'] as int,
username: json['username'] as String,
avatarUrl: json['avatar_url'] as String?,
followersCount: (json['followers_count'] as int?) ?? 0,
isVerified: (json['is_verified'] as bool?) ?? false,
);
}json_serializable 包
对于有许多模型的项目,json_serializable 包通过代码生成消除了重复的 fromJson/toJson 样板代码。设置包括三个步骤:
步骤 1:pubspec.yaml 设置
dependencies:
flutter:
sdk: flutter
json_annotation: ^4.8.1
dev_dependencies:
build_runner: ^2.4.0
json_serializable: ^6.7.0步骤 2:为模型添加注解
import 'package:json_annotation/json_annotation.dart';
part 'product.g.dart';
@JsonSerializable()
class Product {
final int id;
final String name;
@JsonKey(name: 'unit_price')
final double unitPrice;
@JsonKey(defaultValue: true)
final bool inStock;
const Product({
required this.id,
required this.name,
required this.unitPrice,
required this.inStock,
});
factory Product.fromJson(Map<String, dynamic> json) =>
_$ProductFromJson(json);
Map<String, dynamic> toJson() => _$ProductToJson(this);
}步骤 3:运行 Build Runner
# 一次性生成
flutter pub run build_runner build
# 监视模式——文件保存时自动重新生成
flutter pub run build_runner watch --delete-conflicting-outputs运行 build_runner 后,会创建包含完整 fromJson 和 toJson 实现的 product.g.dart 文件。永远不要编辑此文件——始终编辑带注解的类并重新生成。
常用 @JsonKey 选项:
@JsonKey(name: 'snake_key')——将 JSON 键映射到名称不同的 Dart 字段。@JsonKey(defaultValue: 0)——键缺失时提供默认值。@JsonKey(ignore: true)——从序列化中排除字段。@JsonKey(fromJson: _parseDate, toJson: _formatDate)——自定义转换函数。
freezed 包:不可变数据类
freezed 包构建在 json_serializable 之上,创建具有值相等性、copyWith() 和密封类支持的完全不可变 Dart 类。它是 Riverpod 和 BLoC 状态管理模型的首选:
dev_dependencies:
build_runner: ^2.4.0
json_serializable: ^6.7.0
freezed: ^2.4.0
dependencies:
freezed_annotation: ^2.4.0
json_annotation: ^4.8.1import 'package:freezed_annotation/freezed_annotation.dart';
part 'order.freezed.dart';
part 'order.g.dart';
@freezed
class Order with _$Order {
const factory Order({
required int id,
required String status,
required List<OrderItem> items,
double? discount,
@Default(false) bool isPriority,
}) = _Order;
factory Order.fromJson(Map<String, dynamic> json) =>
_$OrderFromJson(json);
}freezed 的优势:
- 值相等性:具有相同字段的两个 Order 对象无需编写 hashCode 或 == 即可相等。
- copyWith():轻松创建修改后的副本——
order.copyWith(status: 'shipped')。 - @Default:内联声明默认字段值。
- 密封类/联合类型:用模式匹配对 Loading | Data | Error 等状态建模。
@freezed
sealed class ApiState<T> with _$ApiState<T> {
const factory ApiState.loading() = _Loading;
const factory ApiState.data(T value) = _Data;
const factory ApiState.error(String message) = _Error;
}手动 fromJson/toJson 模式
有时你需要完全控制反序列化。以下是常见的手动模式:
DateTime 解析
factory Event.fromJson(Map<String, dynamic> json) {
return Event(
id: json['id'] as int,
title: json['title'] as String,
startTime: DateTime.parse(json['start_time'] as String),
endTime: json['end_time'] != null
? DateTime.parse(json['end_time'] as String)
: null,
updatedAt: DateTime.fromMillisecondsSinceEpoch(
(json['updated_at'] as int) * 1000,
),
);
}
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'start_time': startTime.toIso8601String(),
'end_time': endTime?.toIso8601String(),
'updated_at': updatedAt.millisecondsSinceEpoch ~/ 1000,
};Map 字段
factory Config.fromJson(Map<String, dynamic> json) {
return Config(
settings: (json['settings'] as Map<String, dynamic>)
.map((k, v) => MapEntry(k, v as String)),
metadata: json['metadata'] as Map<String, dynamic>? ?? {},
);
}嵌套对象和列表
大多数真实世界的 JSON 都有嵌套对象和数组。以下是正确处理它们的方法:
class Author {
final int id;
final String name;
Author({required this.id, required this.name});
factory Author.fromJson(Map<String, dynamic> json) =>
Author(id: json['id'] as int, name: json['name'] as String);
Map<String, dynamic> toJson() => {'id': id, 'name': name};
}
class Post {
final int id;
final List<String> tags;
final Author author;
Post({required this.id, required this.tags, required this.author});
factory Post.fromJson(Map<String, dynamic> json) {
return Post(
id: json['id'] as int,
tags: List<String>.from(json['tags'] as List),
author: Author.fromJson(json['author'] as Map<String, dynamic>),
);
}
Map<String, dynamic> toJson() => {
'id': id,
'tags': tags,
'author': author.toJson(),
};
}对于复杂对象列表:
factory Cart.fromJson(Map<String, dynamic> json) {
return Cart(
id: json['id'] as int,
items: (json['items'] as List)
.map((item) => CartItem.fromJson(item as Map<String, dynamic>))
.toList(),
discounts: json['discounts'] != null
? (json['discounts'] as List)
.map((d) => Discount.fromJson(d as Map<String, dynamic>))
.toList()
: null,
);
}枚举序列化
将 Dart 枚举序列化为 JSON 字符串并反序列化需要谨慎——特别是当 API 可能返回尚未定义的值时:
enum OrderStatus {
@JsonValue('pending') pending,
@JsonValue('processing') processing,
@JsonValue('shipped') shipped,
@JsonValue('delivered') delivered,
@JsonValue('cancelled') cancelled,
}使用 json_serializable 时,用 @JsonKey(unknownEnumValue:) 优雅地处理未知值:
@JsonSerializable()
class Order {
final int id;
@JsonKey(unknownEnumValue: OrderStatus.pending)
final OrderStatus status;
Order({required this.id, required this.status});
factory Order.fromJson(Map<String, dynamic> json) =>
_$OrderFromJson(json);
Map<String, dynamic> toJson() => _$OrderToJson(this);
}对于手动序列化,扩展方法效果很好:
extension OrderStatusExtension on String {
OrderStatus toOrderStatus() {
switch (this) {
case 'processing': return OrderStatus.processing;
case 'shipped': return OrderStatus.shipped;
case 'delivered': return OrderStatus.delivered;
case 'cancelled': return OrderStatus.cancelled;
default: return OrderStatus.pending;
}
}
}
status: (json['status'] as String).toOrderStatus(),日期和时间处理
DateTime 处理是 JSON 序列化中最复杂的部分之一。以下是常见模式和可复用的自定义转换器:
class DateTimeConverter implements JsonConverter<DateTime, String> {
const DateTimeConverter();
@override
DateTime fromJson(String json) => DateTime.parse(json);
@override
String toJson(DateTime object) => object.toIso8601String();
}
@JsonSerializable()
class Article {
final int id;
@DateTimeConverter()
final DateTime publishedAt;
Article({required this.id, required this.publishedAt});
factory Article.fromJson(Map<String, dynamic> json) =>
_$ArticleFromJson(json);
Map<String, dynamic> toJson() => _$ArticleToJson(this);
}使用 Dio 和 http 进行网络请求
将 JSON 解析与 HTTP 客户端结合是最常见的 Flutter 使用场景。以下是安全获取和解析 JSON 的方法:
使用 http 包
Future<User> fetchUser(int id) async {
final response = await http.get(
Uri.parse('https://api.example.com/users/$id'),
);
if (response.statusCode == 200) {
return User.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
);
}
throw Exception('HTTP ${response.statusCode}');
}使用 Dio
Future<User> fetchUser(int id) async {
try {
final response = await _dio.get<Map<String, dynamic>>('/users/$id');
return User.fromJson(response.data!);
} on DioException catch (e) {
throw Exception('网络错误: ${e.message}');
}
}状态管理集成
生成的 Dart 模型与 Riverpod 和 BLoC 自然集成——这是 Flutter 最流行的两个状态管理解决方案。
带 AsyncNotifier 的 Riverpod
@riverpod
class UserNotifier extends _$UserNotifier {
@override
Future<User> build(int userId) => _fetchUser(userId);
Future<User> _fetchUser(int id) async {
final response = await http.get(
Uri.parse('https://api.example.com/users/$id'),
);
return User.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
);
}
}BLoC 模式
class UserBloc extends Bloc<UserEvent, UserState> {
UserBloc() : super(const UserState.initial()) {
on<FetchUser>(_onFetchUser);
}
Future<void> _onFetchUser(
FetchUser event, Emitter<UserState> emit,
) async {
emit(const UserState.loading());
try {
final response = await http.get(
Uri.parse('https://api.example.com/users/${event.id}'),
);
final user = User.fromJson(
jsonDecode(response.body) as Map<String, dynamic>,
);
emit(UserState.loaded(user));
} catch (e) {
emit(UserState.error(e.toString()));
}
}
}Hive 本地存储
Hive 是 Flutter 的快速轻量级本地数据库。你可以使用 HiveObject 和 TypeAdapter 持久化 JSON 派生的模型:
@HiveType(typeId: 0)
class CachedUser extends HiveObject {
@HiveField(0) final int id;
@HiveField(1) final String name;
@HiveField(2) final String? email;
CachedUser({required this.id, required this.name, this.email});
factory CachedUser.fromUser(User user) =>
CachedUser(id: user.id, name: user.name, email: user.email);
}Future<void> cacheUser(User user) async {
final box = Hive.box<CachedUser>('users');
await box.put(user.id, CachedUser.fromUser(user));
}
CachedUser? getUser(int id) =>
Hive.box<CachedUser>('users').get(id);测试 JSON 模型
往返测试确保序列化和反序列化互为逆操作。为每个模型编写一个:
void main() {
group('User model', () {
final sampleJson = {
'id': 1,
'name': 'Alice',
'email': 'alice@example.com',
'created_at': '2024-01-15T10:30:00.000Z',
};
test('fromJson 创建正确的 User', () {
final user = User.fromJson(sampleJson);
expect(user.id, equals(1));
expect(user.name, equals('Alice'));
});
test('往返保留所有字段', () {
final user = User.fromJson(sampleJson);
final restored = User.fromJson(user.toJson());
expect(restored.id, equals(user.id));
expect(restored.name, equals(user.name));
});
test('处理 null email', () {
final jsonWithoutEmail = {...sampleJson}..remove('email');
expect(User.fromJson(jsonWithoutEmail).email, isNull);
});
});
}常见错误
以下是 Flutter 开发者在 JSON 序列化中最常见的错误:
1. 忘记运行 build_runner
添加或修改 @JsonSerializable() 类后,始终运行 `flutter pub run build_runner build --delete-conflicting-outputs`。.g.dart 文件不会自动更新。
2. 使用 dynamic 而非类型转换
// 不好——运行时静默传递错误类型
final name = json['name']; // dynamic// 好——用清晰的转换错误快速失败
final name = json['name'] as String;3. 将缺失键视为 null
// 安全处理缺失键
final value = (json['maybe_missing'] ?? '') as String;4. 列表类型擦除
// 不好——List<dynamic>,不是 List<String>
final tags = json['tags'] as List<String>; // 运行时 ClassCastException// 好——显式转换
final tags = List<String>.from(json['tags'] as List);5. 缺少 part 指令
json_serializable 和 freezed 都需要 part 指令。json_serializable:`part 'filename.g.dart';`。freezed 需要两个:`part 'filename.freezed.dart';` 和 `part 'filename.g.dart';`。
常见问题
json_serializable 和 freezed 有什么区别?
json_serializable 为可变类生成 fromJson 和 toJson 方法。freezed 构建在 json_serializable 之上,创建具有值相等性(== 和 hashCode)、copyWith() 和密封联合类型的不可变类。简单模型用 json_serializable,需要不可变性或状态管理联合类型时用 freezed。
每次修改模型都需要运行 build_runner 吗?
是的,但你可以使用监视模式避免手动运行:`flutter pub run build_runner watch --delete-conflicting-outputs`。这会监视文件并在保存时自动重新生成代码。
如何处理 JSON snake_case 键和 Dart camelCase 字段?
使用 json_serializable 时,在类上添加 @JsonSerializable(fieldRename: FieldRename.snake)。这会自动将 snake_case JSON 键映射到 camelCase Dart 字段名,无需在每个字段上单独使用 @JsonKey(name:) 注解。
如何解析顶层 JSON 数组?
当 API 在根级别返回 JSON 数组时,将其解码为 List 并映射每个元素:`final list = jsonDecode(response.body) as List; return list.map((e) => User.fromJson(e as Map<String, dynamic>)).toList();`
JSON 中缺少必填字段会发生什么?
如果用 `json['field'] as String` 转换,而键不存在,json['field'] 返回 null,转换会在运行时抛出类型转换错误。对可能缺失的字段始终使用空安全转换 (`as String?`),或用 `?? defaultValue` 提供默认值。
能在 Flutter Web 中使用 json_serializable 吗?
可以,json_serializable 与平台无关,在 Flutter Mobile、Web 和 Desktop 上的工作方式完全相同。生成的代码只使用 dart:core 类型。
如何序列化继承自另一个类的类?
在 @JsonSerializable 上使用 explicitToJson: true 选项并手动调用 super.toJson(),或者改用组合而非继承。json_serializable 不会自动包含父类字段,因此需要在 toJson() 中显式添加,或在父类上也使用 @JsonSerializable。
我应该为每个 API 响应生成 Dart 模型吗?
对于多次访问、在函数间传递或存储在状态中的任何 JSON 结构,都应生成模型。对于只读取一两个字段的一次性 API 调用,直接使用 jsonDecode 和 Map 访问是可以的。随着项目增长,类型化模型几乎总是值得的。
试用我们的免费 JSON 转 Dart 工具
JSON to Dart →