JSON 和序列化数据

大多数移动应用都需要与 web 服务器通信,同时在某些时候轻松地存储结构化数据。当创造需要网络连接的应用时,它迟早需要处理一些常见的 JSON。

It is hard to think of a mobile app that doesn’t need to communicate with a web server or easily store structured data at some point. When making network-connected apps, the chances are that it needs to consume some good old JSON, sooner or later.

本指南介绍了如何在 Flutter 中使用 JSON。包括了如何在不同场景中使用相应的 JSON 解决方案,以及为什么要这么做。

This guide looks into ways of using JSON with Flutter. It covers which JSON solution to use in different scenarios, and why.

我需要哪一种 JSON 序列化数据方法?

Which JSON serialization method is right for me?

本文涵盖了两种常规的 JSON 使用策略:

This article covers two general strategies for working with JSON:

  • 手动序列化数据

    Manual serialization

  • 利用代码生成进行自动序列化数据

    Automated serialization using code generation

不同的项目复杂度不同,用例也不一样。对于较小的概念验证项目或者快速原型,使用代码生成器可能有些过于繁杂。对于具有很多更加复杂的 JSON 模型的应用,手动编码可能很快变得无聊、重复并且出现很多小错误。

Different projects come with different complexities and use cases. For smaller proof-of-concept projects or quick prototypes, using code generators might be overkill. For apps with several JSON models with more complexity, encoding by hand can quickly become tedious, repetitive, and lend itself to many small errors.

为较小的项目使用手动序列化数据

Use manual serialization for smaller projects

手动 JSON 解码是指在 dart:convert 中使用内置的 JSON 解码器。它包括将原始 JSON 字符串传递给 jsonDecode() 方法,然后在产生的 Map<String, dynamic> 计算结果中寻找你需要的值。它没有外部依赖或者特定的设置过程,这有利于快速证明概念。

Manual JSON decoding refers to using the built-in JSON decoder in dart:convert. It involves passing the raw JSON string to the jsonDecode() function, and then looking up the values you need in the resulting Map<String, dynamic>. It has no external dependencies or particular setup process, and it’s good for a quick proof of concept.

当你的项目变大时,手动解码表现得并不理想。手动编写解码逻辑会变得难以管理并容易出错。如果你产生了笔误去获取一个不存在的 JSON 字段,你的代码会在运行时抛出一个错误。

Manual decoding does not perform well when your project becomes bigger. Writing decoding logic by hand can become hard to manage and error-prone. If you have a typo when accessing a nonexistent JSON field, your code throws an error during runtime.

如果你的项目没有很多的 JSON 模型并且你正在寻找一个快速测试概念的方法,手动序列化数据可能是你要的开始的方式。关于手动编码的示例,请参阅 使用 dart:convert 手动序列化 JSON 数据

If you do not have many JSON models in your project and are looking to test a concept quickly, manual serialization might be the way you want to start. For an example of manual encoding, see Serializing JSON manually using dart:convert.

为中大型项目使用代码生成

Use code generation for medium to large projects

利用代码生成的 JSON 序列化数据,意味着可以通过外部的库生成编码模板。在一些初始化设置后,你可以运行文件监听程序,来从你的模型类生成代码。例如,json_serializablebuilt_value 就是这类的库。

JSON serialization with code generation means having an external library generate the encoding boilerplate for you. After some initial setup, you run a file watcher that generates the code from your model classes. For example, json_serializable and built_value are these kinds of libraries.

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

This approach scales well for a larger project. No hand-written boilerplate is needed, and typos when accessing JSON fields are caught at compile-time. The downside with code generation is that it requires some initial setup. Also, the generated source files might produce visual clutter in your project navigator.

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

You might want to use generated code for JSON serialization when you have a medium or a larger project. To see an example of code generation based JSON encoding, see Serializing JSON using code generation libraries.

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

Is there a GSON/Jackson/Moshi equivalent in Flutter?

简单来说,没有。

The simple answer is no.

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

Such a library would require using runtime reflection, which is disabled in Flutter. Runtime reflection interferes with tree shaking, which Dart has supported for quite a long time. With tree shaking, you can “shake off” unused code from your release builds. This optimizes the app’s size significantly.

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

Since reflection makes all code implicitly used by default, it makes tree shaking difficult. The tools cannot know what parts are unused at runtime, so the redundant code is hard to strip away. App sizes cannot be easily optimized when using reflection.

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

Although you cannot use runtime reflection with Flutter, some libraries give you similarly easy-to-use APIs but are based on code generation instead. This approach is covered in more detail in the code generation libraries section.

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

Serializing JSON manually using dart:convert

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

Basic JSON serialization in Flutter is very simple. Flutter has a built-in dart:convert library that includes a straightforward JSON encoder and decoder.

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

The following sample JSON implements a simple user model.

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

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

With dart:convert, you can serialize this JSON model in two ways.

内联序列化 JSON 数据

Serializing JSON inline

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

By looking at the dart:convert documentation, you’ll see that you can decode the JSON by calling the jsonDecode() function, with the JSON string as the method argument.

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

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

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

Unfortunately, jsonDecode() returns a Map<String, dynamic>, meaning that you do not know the types of the values until runtime. With this approach, you lose most of the statically typed language features: type safety, autocompletion and most importantly, compile-time exceptions. Your code will become instantly more error-prone.

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

For example, whenever you access the name or email fields, you could quickly introduce a typo. A typo that the compiler doesn’t know about since the JSON lives in a map structure.

在模型类中序列化 JSON 数据

Serializing JSON inside model classes

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

Combat the previously mentioned problems by introducing a plain model class, called User in this example. Inside the User class, you’ll find:

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

    A User.fromJson() constructor, for constructing a new User instance from a map structure.

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

    A toJson() method, which converts a User instance into a map.

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

With this approach, the calling code can have type safety, autocompletion for the name and email fields, and compile-time exceptions. If you make typos or treat the fields as ints instead of Strings, the app won’t compile, instead of crashing at runtime.

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 实例。

The responsibility of the decoding logic is now moved inside the model itself. With this new approach, you can decode a user easily.

Map 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() 已经帮你做了这件事。

To encode a user, pass the User object to the jsonEncode() function. You don’t need to call the toJson() method, since jsonEncode() already does it for you.

String json = jsonEncode(user);

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

With this approach, the calling code doesn’t have to worry about JSON serialization at all. However, the model class still definitely has to. In a production app, you would want to ensure that the serialization works properly. In practice, the User.fromJson() and User.toJson() methods both need to have unit tests in place to verify correct behavior.

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

However, real-world scenarios are not usually that simple. Sometimes JSON API responses are more complex, for example since they contain nested JSON objects that must be parsed through their own model class.

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

It would be nice if there were something that handled the JSON encoding and decoding for you. Luckily, there is!

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

Serializing JSON using code generation libraries

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

Although there are other libraries available, this guide uses json_serializable, an automated source code generator that generates the JSON serialization boilerplate for you.

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

Since the serialization code is not handwritten or maintained manually anymore, you minimize the risk of having JSON serialization exceptions at runtime.

在项目中设置 json_serializable

Setting up json_serializable in a project

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

To include json_serializable in your project, you need one regular dependency, and two dev dependencies. In short, dev dependencies are dependencies that are not included in our app source code—they are only used in the development environment.

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

The latest versions of these required dependencies can be seen by following the pubspec file in the JSON serializable example.

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)以确保在你的项目中可以使用这些新的依赖。

Run flutter pub get inside your project root folder (or click Packages get in your editor) to make these new dependencies available in your project.

以 json_serializable 的方式创建模型类

Creating model classes the json_serializable way

下面显示了怎样将 User 类转换为 json_serializable 后的类。简单起见,该代码使用了前面的例子中的简化的 JSON 模型。

The following shows how to convert the User class to a json_serializable class. For the sake of simplicity, this code uses the simplified JSON model from the previous samples.

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 字段的代码。

With this setup, the source code generator generates code for encoding and decoding the name and email fields from JSON.

如果需要,你可以很轻易地自定义命名策略。例如,如果 API 返回带有 蛇形命名方式 的对象,并且你想要在你的模型里使用 小驼峰 的命名方式,你可以使用带有一个 name 参数的 @JsonKey 注解。

If needed, it is also easy to customize the naming strategy. For example, if the API returns objects with snake_case, and you want to use lowerCamelCase in your models, you can use the @JsonKey annotation with a name parameter:

/// 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 键值。

It’s best if both server and client follow the same naming strategy.
@JsonSerializable() provides fieldRename enum for totally converting dart fields into JSON keys.

定义 @JsonSerializable(fieldRename: FieldRename.snake) 与添加 @JsonKey(name: '<snake_case>') 到每一个字段是同样的效果。

Modifying @JsonSerializable(fieldRename: FieldRename.snake) is equivalent to adding @JsonKey(name: '<snake_case>') to each field.

服务端的数据有时无法确认,所以在客户端很有必要进行数据校验和保护。其他常见的 @JsonKey 声明方法包括:

Sometimes server data is uncertain, so it is necessary to verify and protect data on client.
Other commonly used @JsonKey annotations include:

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

运行代码生成工具

Running the code generation utility

当你首次创建 json_serializable 类时,你会得到类似下图的错误。

When creating json_serializable classes the first time, you’ll get errors similar to what is shown in the image below.

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

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

These errors are entirely normal and are simply because the generated code for the model class does not exist yet. To resolve this, run the code generator that generates the serialization boilerplate.

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

There are two ways of running the code generator.

一次性代码生成

One-time code generation

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

By running flutter pub run build_runner build in the project root, you generate JSON serialization code for your models whenever they are needed. This triggers a one-time build that goes through the source files, picks the relevant ones, and generates the necessary serialization code for them.

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

While this is convenient, it would be nice if you did not have to run the build manually every time you make changes in your model classes.

持续生成代码

Generating code continuously

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

A watcher makes our source code generation process more convenient. It watches changes in our project files and automatically builds the necessary files when needed. Start the watcher by running flutter pub run build_runner watch in the project root.

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

It is safe to start the watcher once and leave it running in the background.

使用 json_serializable 模型

Consuming json_serializable models

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

To decode a JSON string the json_serializable way, you do not have actually to make any changes to our previous code.

Map userMap = jsonDecode(jsonString);
var user = User.fromJson(userMap);

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

The same goes for encoding. The calling API is the same as before.

String json = jsonEncode(user);

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

With json_serializable, you can forget any manual JSON serialization in the User class. The source code generator creates a file called user.g.dart, that has all the necessary serialization logic. You no longer have to write automated tests to ensure that the serialization works—it’s now the library’s responsibility to make sure the serialization works appropriately.

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

Generating code for nested classes

你可能类在代码中用了嵌套类,在你把类作为参数传递给一些服务(比如 Firebase)的时候,你可能会遇到 Invalid argument 错误。

You might have code that has nested classes within a class. If that is the case, and you have tried to pass the class in JSON format as an argument to a service (such as Firebase, for example), you might have experienced an Invalid argument error.

比如下面的这个 Address 类:

Consider the following Address class:

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 类中使用:

The Address class is nested inside the User class:

import 'address.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';

@JsonSerializable()
class User {
  String firstName;
  Address address;

  User(this.firstName, this.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() 看起来会像下面这样:

Running flutter pub run build_runner build in the terminal creates the *.g.dart file, but the private _$UserToJson() function looks something like the following:

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

现在看起来并没有什么问题,但当你想要打印 (print()) 这个用户对象时:

All looks fine now, but if you do a print() on the user object:

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

结果会是:

The result is:

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

而你期望的输出结果是这样的:

When what you probably want is output like the following:

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

为了得到正常的输出,你需要在类声明之前在 @JsonSerializable 方法加入 explicitToJson: true 参数, User 类现在看起来是这样的:

To make this work, pass explicitToJson: true in the @JsonSerializable() annotation over the class declaration. The User class now looks as follows:

import 'address.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';

@JsonSerializable(explicitToJson: true)
class User {
  String firstName;
  Address address;

  User(this.firstName, this.address);

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

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

For more information, see explicitToJson in the JsonSerializable class for the json_annotation package.

进一步参考

Further references

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

For more information, see the following resources: