跳到主内容

JSON 和序列化

如何在 Flutter 中使用 JSON。

很难想象有一个移动应用在某个时刻不需要与 Web 服务器通信或存储结构化数据。在开发联网应用时,迟早都需要处理传统的 JSON 数据。

本指南探讨了在 Flutter 中使用 JSON 的方法。它涵盖了在不同场景下应使用哪种 JSON 解决方案,以及原因。

在 YouTube 上观看(在新标签页打开):“dart:convert (每周技术分享)”

哪种 JSON 序列化方法适合我?

#

本文涵盖了处理 JSON 的两种通用策略:

  • 手动序列化
  • 使用代码生成进行自动化序列化

不同的项目具有不同的复杂性和使用场景。对于较小的概念验证项目或快速原型,使用代码生成器可能有些大材小用。对于具有多个复杂 JSON 模型的应用,手动编码很快会变得乏味、重复,且容易产生各种小错误。

小型项目使用手动序列化

#

手动 JSON 解码是指使用 dart:convert 中内置的 JSON 解码器。它涉及将原始 JSON 字符串传递给 jsonDecode() 函数,然后在生成的 Map<String, dynamic> 中查找所需的值。它没有外部依赖,也不需要特别的配置过程,非常适合快速概念验证。

当项目变大时,手动解码的表现不佳。手动编写解码逻辑变得难以管理且容易出错。如果访问不存在的 JSON 字段时出现拼写错误,代码会在运行时抛出错误。

如果你的项目中 JSON 模型不多,并且想快速测试一个概念,手动序列化可能是你的首选。关于手动编码的示例,请参阅 使用 dart:convert 手动序列化 JSON

中大型项目使用代码生成

#

使用代码生成的 JSON 序列化意味着由外部库为你生成编码样板代码。在进行一些初始设置后,你可以运行一个文件监视器,根据你的模型类生成代码。例如,json_serializablebuilt_value 就是这类库。

这种方法非常适合大型项目。无需手写样板代码,访问 JSON 字段时的拼写错误会在编译时被捕获。代码生成的缺点是需要一些初始设置,并且生成的源文件可能会在项目导航器中造成视觉混乱。

当你拥有中型或大型项目时,可能需要使用生成的代码进行 JSON 序列化。要查看基于代码生成的 JSON 编码示例,请参阅 使用代码生成库序列化 JSON

Flutter 中有 GSON/Jackson/Moshi 的等效库吗?

#

简单的回答是:没有。

这样的库需要使用运行时 反射 (reflection),但在 Flutter 中是被禁用的。运行时反射会干扰 摇树优化 (tree shaking),而 Dart 对此支持已久。通过摇树优化,你可以从发布版本中“剔除”未使用的代码。这显著优化了应用的大小。

由于反射使得所有代码在默认情况下都是隐式使用的,因此它使摇树优化变得困难。工具无法知道哪些部分在运行时未被使用,因此难以剔除冗余代码。使用反射时,应用大小无法轻易优化。

尽管你无法在 Flutter 中使用运行时反射,但有些库提供了同样易于使用的 API,但它们是基于代码生成的。这种方法在 代码生成库 一节中有更详细的介绍。

使用 dart:convert 手动序列化 JSON

#

Flutter 中的基本 JSON 序列化非常简单。Flutter 有一个内置的 dart:convert 库,其中包含一个直接的 JSON 编码器和解码器。

以下示例 JSON 实现了一个简单的用户模型。

json
{
  "name": "John Smith",
  "email": "john@example.com"
}

使用 dart:convert,你可以通过两种方式序列化此 JSON 模型。

内联序列化 JSON

#

查看 dart:convert 文档,你会发现可以通过调用 jsonDecode() 函数并传入 JSON 字符串作为参数来解码 JSON。

dart
final user = jsonDecode(jsonString) as Map<String, dynamic>;

print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');

不幸的是,jsonDecode() 返回的是一个 dynamic,这意味着在运行时之前你无法确定值的类型。使用这种方法,你失去了大部分静态类型语言的特性:类型安全、自动补全,最重要的是,编译时异常。你的代码会瞬间变得更容易出错。

例如,每当你访问 nameemail 字段时,都可能引发拼写错误。由于 JSON 存在于 Map 结构中,编译器无法识别这种拼写错误。

在模型类中序列化 JSON

#

通过引入一个简单的模型类(在本例中称为 User)来解决上述问题。在 User 类中,你会发现:

  • 一个 User.fromJson() 构造函数,用于从 Map 结构构造一个新的 User 实例。
  • 一个 toJson() 方法,将 User 实例转换为 Map。

通过这种方法,调用代码 可以拥有类型安全、nameemail 字段的自动补全以及编译时异常。如果你拼写错误或将字段当作 int 而不是 String 处理,应用将无法编译,而不是在运行时崩溃。

user.dart

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};
}

解码逻辑的责任现在被移到了模型内部。使用这种新方法,你可以轻松解码用户。

dart
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() 已经为你完成了。

dart
String json = jsonEncode(user);

使用这种方法,调用代码完全不需要担心 JSON 序列化。然而,模型类显然必须担心。在生产环境中,你希望确保序列化正常工作。在实践中,User.fromJson()User.toJson() 方法都需要进行单元测试,以验证其行为是否正确。

然而,现实场景并不总是那么简单。有时 JSON API 响应更为复杂,例如,因为它们包含必须通过各自模型类解析的嵌套 JSON 对象。

如果能有一些东西自动帮你处理 JSON 编码和解码就太好了。幸运的是,确实有!

使用代码生成库序列化 JSON

#

虽然还有其他库可用,但本指南使用 json_serializable,这是一个自动源代码生成器,可以为你生成 JSON 序列化样板代码。

由于序列化代码不再是手写或手动维护的,你将运行时出现 JSON 序列化异常的风险降至最低。

在项目中配置 json_serializable

#

要在你的项目中包含 json_serializable,你需要一个常规依赖项和两个 开发依赖项 (dev dependencies)。简而言之,开发依赖项 是指不包含在应用源代码中的依赖项——它们仅在开发环境中使用。

要添加依赖项,请运行 flutter pub add

flutter pub add json_annotation dev:build_runner dev:json_serializable

在项目根文件夹中运行 flutter pub get(或在编辑器中点击 Packages get),使这些新依赖项在项目中可用。

以 json_serializable 方式创建模型类

#

以下展示了如何将 User 类转换为 json_serializable 类。为了简单起见,此代码使用前面示例中的简化 JSON 模型。

user.dart

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 编码和解码 nameemail 字段的代码。

如果需要,自定义命名策略也很容易。例如,如果 API 返回的对象使用 snake_case,而你希望在模型中使用 lowerCamelCase,你可以使用 @JsonKey 注解并带有 name 参数。

dart
/// 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 注解包括:

dart
/// 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 类时,你会遇到类似以下的错误:

Target of URI hasn't been generated: 'user.g.dart'.

这些错误完全正常,只是因为模型类的生成代码尚不存在。要解决此问题,请运行生成序列化样板代码的代码生成器。

有两种运行代码生成器的方法。

一次性代码生成

#

通过在项目根目录运行 dart run build_runner build --delete-conflicting-outputs,你可以在需要时为模型生成 JSON 序列化代码。这会触发一次性构建,遍历源文件,挑选相关文件,并为它们生成必要的序列化代码。

虽然这很方便,但如果你每次更改模型类时都不必手动运行构建,那就更好了。

持续生成代码

#

监视器 (watcher) 使我们的源代码生成过程更加方便。它会监视项目中文件的更改,并在需要时自动构建必要的文件。通过在项目根目录运行 dart run build_runner watch --delete-conflicting-outputs 启动监视器。

启动监视器并在后台运行它是安全的。

使用 json_serializable 模型

#

要以 json_serializable 方式解码 JSON 字符串,实际上不需要对之前的代码做任何更改。

dart
final userMap = jsonDecode(jsonString) as Map<String, dynamic>;
final user = User.fromJson(userMap);

编码也是如此。调用 API 与之前相同。

dart
String json = jsonEncode(user);

使用 json_serializable,你可以忘记 User 类中的任何手动 JSON 序列化。源代码生成器会创建一个名为 user.g.dart 的文件,其中包含所有必要的序列化逻辑。你不再需要编写自动化测试来确保序列化正常工作——现在确保序列化正常工作是 库的责任

为嵌套类生成代码

#

你的代码中可能包含嵌套类。如果是这样,并且你尝试以 JSON 格式将该类作为参数传递给服务(例如 Firebase),你可能遇到过 Invalid argument 错误。

考虑以下 Address 类:

dart
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 类中:

dart
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() 函数看起来像下面这样:

dart
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
  'name': instance.name,
  'address': instance.address,
};

现在看起来一切正常,但如果你对 user 对象执行 print()

dart
Address address = Address('My st.', 'New York');
User user = User('John', address);
print(user.toJson());

结果是:

json
{name: John, address: Instance of 'address'}

而你可能想要的输出是:

json
{name: John, address: {street: My st., city: New York}}

要实现这一点,请在类声明上方的 @JsonSerializable() 注解中传入 explicitToJson: true。现在的 User 类如下所示:

dart
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);
}

有关更多信息,请参阅 json_annotation 包中 JsonSerializable 类的 explicitToJson 说明。

更多参考

#

有关更多信息,请参阅以下资源: