JSON 和序列化
很难想到一款移动应用程序不需要在某个时间点与 Web 服务器通信或轻松存储结构化数据。在制作网络连接应用程序时,迟早需要使用一些好的旧 JSON。
本指南探讨了在 Flutter 中使用 JSON 的方法。它涵盖了在不同场景中使用哪种 JSON 解决方案,以及原因。
哪种 JSON 序列化方法适合我?
本文介绍了两种使用 JSON 的通用策略
- 手动序列化
- 使用代码生成进行自动序列化
不同的项目具有不同的复杂性和用例。对于较小的概念验证项目或快速原型,使用代码生成器可能过于繁琐。对于具有多个复杂 JSON 模型的应用,手动编码很快会变得乏味、重复,并容易出现许多小错误。
对较小的项目使用手动序列化
手动 JSON 解码是指使用 dart:convert
中的内置 JSON 解码器。它涉及将原始 JSON 字符串传递给 jsonDecode()
函数,然后在生成的 Map<String, dynamic>
中查找所需的值。它没有外部依赖关系或特定的设置过程,并且适用于快速概念验证。
当项目变大时,手动解码效果不佳。手动编写解码逻辑可能会变得难以管理且容易出错。如果在访问不存在的 JSON 字段时出现错别字,则代码会在运行时引发错误。
如果项目中没有很多 JSON 模型,并且希望快速测试一个概念,则手动序列化可能是你想要开始的方式。有关手动编码的示例,请参阅 使用 dart:convert 手动序列化 JSON。
对中大型项目使用代码生成
使用代码生成的 JSON 序列化意味着让外部库为你生成编码样板。在进行一些初始设置后,运行一个文件观察器,该观察器会根据你的模型类生成代码。例如,json_serializable
和 built_value
就是这类库。
这种方法非常适合较大的项目。不需要手写样板,并且在访问 JSON 字段时出现的错别字会在编译时被捕获。代码生成的一个缺点是它需要一些初始设置。此外,生成的源文件可能会在项目导航器中产生视觉混乱。
当你有中型或大型项目时,你可能希望对 JSON 序列化使用生成的代码。要查看基于代码生成的 JSON 编码示例,请参阅 使用代码生成库序列化 JSON。
Jackson/Moshi 等价物?
Flutter 中是否有 GSON/简单的答案是否定的。
此类库需要使用运行时 反射,而 Flutter 中禁用此功能。运行时反射会干扰 tree shaking,而 Dart 已支持此功能很长时间了。使用 tree shaking,你可以从发布版本中“清除”未使用的代码。这极大地优化了应用的大小。
由于反射默认情况下会隐式地使用所有代码,因此它会让 tree shaking 变得困难。工具无法知道哪些部分在运行时未被使用,因此难以剥离冗余代码。使用反射时,无法轻松优化应用大小。
虽然你无法在 Flutter 中使用运行时反射,但一些库为你提供了类似的易于使用的 API,但基于代码生成。此方法在 代码生成库 部分中有更详细的介绍。
使用 dart:convert 手动序列化 JSON
Flutter 中的基本 JSON 序列化非常简单。Flutter 有一个内置的 dart:convert
库,其中包含一个直接的 JSON 编码器和解码器。
以下 JSON 样例实现了一个简单的用户模型。
{
"name": "John Smith",
"email": "[email protected]"
}
使用 dart:convert
,你可以通过两种方式序列化此 JSON 模型。
内联序列化 JSON
通过查看 dart:convert
文档,你会看到你可以通过调用 jsonDecode()
函数来解码 JSON,其中 JSON 字符串作为方法参数。
final user = jsonDecode(jsonString) as Map<String, dynamic>;
print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');
不幸的是,jsonDecode()
返回一个 dynamic
,这意味着你不知道值的类型,直到运行时。使用此方法,你将失去大多数静态类型语言特性:类型安全、自动完成,最重要的是,编译时异常。你的代码将立即变得更容易出错。
例如,每当你访问 name
或 email
字段时,你都可以快速引入一个错别字。这是一个编译器不知道的错别字,因为 JSON 存在于一个映射结构中。
在模型类中序列化 JSON
通过引入一个简单的模型类来解决前面提到的问题,在此示例中称为 User
。在 User
类中,您会发现
- 一个
User.fromJson()
构造函数,用于从映射结构构造一个新的User
实例。 - 一个
toJson()
方法,它将User
实例转换为映射。
使用此方法,调用代码可以具有类型安全性、name
和 email
字段的自动完成以及编译时异常。如果您输入错误或将字段视为 int
而不是 String
,则应用程序将无法编译,而不是在运行时崩溃。
user.dart
class User {
final String name;
final String email;
User(this.name, this.email);
User.fromJson(Map<String, dynamic> json)
: name = json['name'] as String,
email = json['email'] as String;
Map<String, dynamic> toJson() => {
'name': name,
'email': email,
};
}
解码逻辑的责任现在已移至模型本身内部。使用这种新方法,您可以轻松地解码用户。
final userMap = jsonDecode(jsonString) as Map<String, dynamic>;
final user = User.fromJson(userMap);
print('Howdy, ${user.name}!');
print('We sent the verification link to ${user.email}.');
要对用户进行编码,请将 User
对象传递给 jsonEncode()
函数。您不必调用 toJson()
方法,因为 jsonEncode()
已经为您执行了此操作。
String json = jsonEncode(user);
使用此方法,调用代码完全不必担心 JSON 序列化。但是,模型类肯定仍然需要。在生产应用程序中,您需要确保序列化正常工作。实际上,User.fromJson()
和 User.toJson()
方法都需要有单元测试来验证正确行为。
但是,现实世界的场景并不总是那么简单。有时 JSON API 响应更复杂,例如,因为它们包含嵌套的 JSON 对象,必须通过它们自己的模型类进行解析。
如果有什么东西可以为您处理 JSON 编码和解码,那就太好了。幸运的是,确实有!
使用代码生成库序列化 JSON
虽然还有其他库可用,但本指南使用 json_serializable
,这是一个自动源代码生成器,可为您生成 JSON 序列化样板。
由于序列化代码不再是手写的或手动维护的,因此您最大程度地降低了在运行时出现 JSON 序列化异常的风险。
在项目中设置 json_serializable
要在您的项目中包含 json_serializable
,您需要一个常规依赖项和两个开发依赖项。简而言之,开发依赖项是不包含在我们的应用程序源代码中的依赖项——它们仅在开发环境中使用。
要添加依赖项,请运行 flutter pub add
$ flutter pub add json_annotation dev:build_runner dev:json_serializable
在您的项目根文件夹中运行 flutter pub get
(或在您的编辑器中单击获取软件包)以使这些新依赖项在您的项目中可用。
以 json_serializable 方式创建模型类
以下显示如何将 User
类转换为 json_serializable
类。为了简单起见,此代码使用前面示例中的简化 JSON 模型。
user.dart
import 'package:json_annotation/json_annotation.dart';
/// This allows the `User` class to access private members in
/// the generated file. The value for this is *.g.dart, where
/// the star denotes the source file name.
part 'user.g.dart';
/// An annotation for the code generator to know that this class needs the
/// JSON serialization logic to be generated.
@JsonSerializable()
class User {
User(this.name, this.email);
String name;
String email;
/// A necessary factory constructor for creating a new User instance
/// from a map. Pass the map to the generated `_$UserFromJson()` constructor.
/// The constructor is named after the source class, in this case, User.
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
/// `toJson` is the convention for a class to declare support for serialization
/// to JSON. The implementation simply calls the private, generated
/// helper method `_$UserToJson`.
Map<String, dynamic> toJson() => _$UserToJson(this);
}
通过此设置,源代码生成器将生成代码,用于从 JSON 中对 name
和 email
字段进行编码和解码。
如有需要,还可以轻松自定义命名策略。例如,如果 API 返回带有 snake_case 的对象,而您希望在模型中使用 lowerCamelCase,则可以使用 @JsonKey
注释和一个名称参数
/// Tell json_serializable that "registration_date_millis" should be
/// mapped to this property.
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;
最好让服务器和客户端都遵循相同的命名策略。
@JsonSerializable()
提供 fieldRename
枚举,用于将 dart 字段完全转换为 JSON 键。
修改 @JsonSerializable(fieldRename: FieldRename.snake)
等效于向每个字段添加 @JsonKey(name: '<snake_case>')
。
有时服务器数据不确定,因此有必要在客户端验证和保护数据。
其他常用的 @JsonKey
注释包括
/// Tell json_serializable to use "defaultValue" if the JSON doesn't
/// contain this key or if the value is `null`.
@JsonKey(defaultValue: false)
final bool isAdult;
/// When `true` tell json_serializable that JSON must contain the key,
/// If the key doesn't exist, an exception is thrown.
@JsonKey(required: true)
final String id;
/// When `true` tell json_serializable that generated code should
/// ignore this field completely.
@JsonKey(ignore: true)
final String verificationCode;
运行代码生成实用程序
首次创建 json_serializable
类时,您将收到类似于下图中所示的错误。
这些错误完全正常,只是因为模型类的生成代码尚不存在。要解决此问题,请运行生成序列化样板的代码生成器。
有两种运行代码生成器的方法。
一次性代码生成
通过在项目根目录中运行 dart run build_runner build --delete-conflicting-outputs
,您可以在需要时为模型生成 JSON 序列化代码。这会触发一次性构建,该构建将遍历源文件,挑选相关文件,并为它们生成必要的序列化代码。
虽然这很方便,但如果您不必在每次更改模型类时都手动运行构建,那就太好了。
持续生成代码
观察程序使我们的源代码生成过程更加方便。它监视项目文件中的更改,并在需要时自动构建必要的文件。通过在项目根目录中运行 dart run build_runner watch --delete-conflicting-outputs
启动观察程序。
可以安全地启动观察程序一次,并让它在后台运行。
使用 json_serializable 模型
要以 json_serializable
方式解码 JSON 字符串,您实际上不必对之前的代码进行任何更改。
final userMap = jsonDecode(jsonString) as Map<String, dynamic>;
final user = User.fromJson(userMap);
编码也是如此。调用 API 与之前相同。
String json = jsonEncode(user);
使用 json_serializable
,您可以在 User
类中忘记任何手动 JSON 序列化。源代码生成器会创建一个名为 user.g.dart
的文件,其中包含所有必要的序列化逻辑。您不必再编写自动化测试来确保序列化正常工作,现在由库负责确保序列化正常工作。
为嵌套类生成代码
您的代码可能在类中包含嵌套类。如果是这种情况,并且您尝试将 JSON 格式的类作为参数传递给服务(例如 Firebase),则您可能会遇到 Invalid argument
错误。
考虑以下 Address
类
import 'package:json_annotation/json_annotation.dart';
part 'address.g.dart';
@JsonSerializable()
class Address {
String street;
String city;
Address(this.street, this.city);
factory Address.fromJson(Map<String, dynamic> json) =>
_$AddressFromJson(json);
Map<String, dynamic> toJson() => _$AddressToJson(this);
}
Address
类嵌套在 User
类中
import 'package:json_annotation/json_annotation.dart';
import 'address.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
User(this.name, this.address);
String name;
Address address;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
在终端中运行 dart run build_runner build --delete-conflicting-outputs
会创建 *.g.dart
文件,但私有 _$UserToJson()
函数看起来像以下内容
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'name': instance.name,
'address': instance.address,
};
现在看起来一切正常,但如果你对用户对象执行 print()
Address address = Address('My st.', 'New York');
User user = User('John', address);
print(user.toJson());
结果是
{name: John, address: Instance of 'address'}
而你可能想要以下内容作为输出
{name: John, address: {street: My st., city: New York}}
要实现此目的,请在类声明上通过 @JsonSerializable()
注解传递 explicitToJson: true
。 User
类现在如下所示
import 'package:json_annotation/json_annotation.dart';
import 'address.dart';
part 'user.g.dart';
@JsonSerializable(explicitToJson: true)
class User {
User(this.name, this.address);
String name;
Address address;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
有关更多信息,请参阅 explicitToJson
,该信息位于 JsonSerializable
类中,适用于 json_annotation
包。
进一步参考
有关更多信息,请参阅以下资源
dart:convert
和JsonCodec
文档json_serializable
包(位于 pub.dev 上)json_serializable
示例(位于 GitHub 上)- 深入了解 Dart 的模式和记录 代码实验室