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_serializablebuilt_value 就是这类的库。

这种方法适用于大型项目。不需要手动编写模板,并且一些试图去获取不存在的 JSON 字段的笔误,会在编译阶段被发现。代码生成的麻烦之处,在于它需要一些初始化设置。并且,生成的源文件可能在你的项目导航中产生一些视觉上的混乱。

当你有一个中大型项目时,你可能想要使用生成的代码来进行 JSON 序列化。要看基于代码生成的 JSON 编码,见 使用代码生成库序列化 JSON 数据

Flutter 中是否有 GSON/Jackson/Moshi 的等价物?

简单来说,没有。

这样的库需要使用运行时进行 反射,这在 Flutter 中是被禁用的。运行时反射会影响 Dart 支持了相当久的 摇树优化。通过 tree shaking,你可以从你的发布版本中“抖掉”不需要使用的代码。这会显著优化 App 的体积。

由于反射会默认让所有的代码被隐式使用,这让 tree shaking 变得困难。工具不知道哪一部分在运行时不会被用到,所以冗余的代码很难被清除。当使用反射时,App 的体积不能被轻易优化。

尽管你不能在 Flutter 中使用运行时反射,还是有一些库提供了基于代码生成的方便使用的 API,这个方法的更多细节在 代码生成库 部分。

使用 dart:convert 手动序列化 JSON 数据

在 Flutter 中基础的序列化 JSON 十分容易的。 Flutter 有一个内置的 dart:convert 的库,这个库包含了一个简单的 JSON 编码器和解码器。

下面的样例实现了一个简单用户模型。

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

通过 dart:convert,你可以用两种方法编码这个 JSON 模型。

内联序列化 JSON 数据

通过查阅 dart:convert 文档,你会看到你可以将 JSON 字符串作为方法的参数,调用 jsonDecode() 方法来解码 JSON。

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

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

不幸的是,jsonDecode() 返回一个 Map<String, dynamic>,这意味着你在运行时以前都不知道值的类型。使用这个方法,你失去了大部分的静态类型语言特性:类型安全、自动补全以及最重要的编译时异常。你的代码会立即变得更加容易出错。

例如,当你获取 name 或者 email 字段,你可能很快引入一个笔误。然而编译器却无法知道映射中是否有 JSON 笔误。

在模型类中序列化 JSON 数据

通过引入一个简单的模型 User 类来解决上面提到的问题。在 User 类中,你会发现:

  • 一个 User.fromJson() 构造函数,用于从映射中构造一个新的 User 实例。

  • 一个 toJson() 方法,这个方法会将 User 实例转换为一个映射。

通过这种方法,调用代码 可以拥有类型安全、 nameemail 字段的自动完成以及编译时异常检测。如果你不小心写错了,或者把 String 类型的字段看成了 int 类型,应用将无法编译,而不是在运行时崩溃。

user.dart

class User {
  final String name;
  final String email;

  User(this.name, this.email);

  User.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        email = json['email'];

  Map<String, dynamic> toJson() => {
        'name': name,
        'email': email,
      };
}

解码逻辑的责任现在转移到了模型内部。通过这个新方法,你可以很容易地解码获得一个 user 实例。

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

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

要对 user 实例进行编码,将 User 对象传到 jsonEncode() 函数中。你不需要调用 toJson() 方法,因为 jsonEncode() 已经帮你做了这件事。

String json = jsonEncode(user);

通过这种方法,被调用的代码根本不需要担心序列化 JSON 数据的问题。然而,你仍然需要模型类。你当然会希望序列化数据在一个生产环境的应用里能奏效。在实践中,User.fromJson()User.toJson() 方法都需要单元测试以便验证正确的行为。

然而,现实场景通常不是那么简单,有时候响应的 JSON API 会更加复杂,例如它可能会包含一些相邻的 JSON 对象,而这些对象同样需要使用它的 model 类进行解析。

如果有一些东西可以帮你处理 JSON 编码和解码就好了。幸运的是,已经有了!

使用代码生成库序列化 JSON 数据

尽管有其它库可以使用,但是本指南使用了 json_serializable,一个自动化源代码生成器来为你生成 JSON 序列化数据模板。

由于序列化数据代码不再需要手动编写或者维护,你可以将序列化 JSON 数据在运行时的异常风险降到最低。

在项目中设置 json_serializable

要在你的项目中包含 json_serializable,你需要一个常规依赖,以及两个 dev 依赖。简单来说,dev 依赖 是不包括在我们的 App 源代码中的依赖——它们只会被用在开发环境中。

在序列化 JSON 数据的例子中,这些需要的依赖的最新版本可以在 pubspec 文件 中查看。

pubspec.yaml

dependencies:
  # Your other regular dependencies here
  json_annotation: <latest_version>

dev_dependencies:
  # Your other dev_dependencies here
  build_runner: <latest_version>
  json_serializable: <latest_version>

在你的项目根文件夹下运行 flutter pub get (或者在你的编辑器中点击 Packages 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 返回带有 蛇形命名方式 的对象,并且你想要在你的模型里使用 小驼峰 的命名方式,你可以使用带有一个 name 参数的 @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 类时,你会得到类似下图的错误。

IDE warning when the generated code for a model class does not exist
yet.

这些错误完全正常,很简单,因为这些模型类的生成代码并不存在。要解决这个问题,你需要运行代码生成器来生成序列化数据模板。

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

一次性代码生成

通过在项目根目录运行命令 flutter pub run build_runner build --delete-conflicting-outputs,你可以在任何需要的时候为你的模型生成 JSON 序列化数据代码。这会触发一次构建,遍历源文件,选择相关的文件,然后为它们生成必须的序列化数据代码。

虽然这样很方便,但是如果你不需要在每次修改了你的模型类后都要手动构建那将会很棒。

持续生成代码

监听器 让我们的源代码生成过程更加方便。它会监听我们项目中的文件变化,并且会在需要的时候自动构建必要的文件。你可以在项目根目录运行 flutter pub run build_runner watch 启动监听。

启动监听并让它留在后台运行是安全的。

使用 json_serializable 模型

为了以 json_serializable 的方式解码 JSON 字符串,你不必对以前的代码做任何的改动。

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

编码也是如此。调用 API 和以前一样。

String json = jsonEncode(user);

在使用了 json_serializable 后,你可以立马忘掉 User 类中所有手动序列化的 JSON 数据。源代码生成器会创建一个名为 user.g.dart 的文件,它包含了所有必须的序列化数据逻辑。你不必再编写自动化测试来确保序列化数据奏效。现在 由库来负责 确保序列化数据能正确地被转换。

为嵌套类 (Nested Classes) 生成代码

你可能类在代码中用了嵌套类,在你把类作为参数传递给一些服务(比如 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);
}

在终端中运行 flutter pub run build_runner build 创建 *.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);
}

了解更多信息,请查阅 json_annotation 这个 package 里的 JsonSerializable 类的 explicitToJson 参数等相关文档。

进一步参考

更多信息,请查看以下资源: