Skip to main content

JSON 和序列化

很难想象一款不需要与网络服务器通信或在某些时候轻松存储结构化数据的移动应用。在创建网络连接的应用程序时,迟早都需要使用一些传统的 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 中是被禁用的。运行时反射会干扰tree shaking,Dart 很早就支持 tree shaking 了。使用 tree shaking,您可以从发布版本中“去除”未使用的代码。这会显著优化应用程序的大小。

由于反射默认使所有代码都隐式使用,因此它使 tree shaking 变得困难。工具无法知道运行时哪些部分未使用,因此很难去除冗余代码。使用反射时,应用程序大小难以轻松优化。

尽管您不能在 Flutter 中使用运行时反射,但有些库为您提供了类似易于使用的 API,但它们基于代码生成。代码生成库 部分将更详细地介绍这种方法。

使用 dart:convert 手动序列化 JSON

#

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

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

json
{
  "name": "John Smith",
  "email": "[email protected]"
}

使用 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 存在于映射结构中。

在模型类中序列化 JSON

#

通过引入一个简单的模型类(在此示例中称为 User)来解决前面提到的问题。在 User 类中,您会发现:

  • 一个 User.fromJson() 构造函数,用于根据映射结构创建一个新的 User 实例。
  • 一个 toJson() 方法,它将 User 实例转换为映射。

使用这种方法,调用代码 可以具有类型安全、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()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(或在您的编辑器中点击Packages get)以使这些新依赖项在您的项目中可用。

使用 json_serializable 创建模型类

#

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

user.dart

dart
import 'package:json_annotation/json_annotation.dart';

/// 这允许 `User` 类访问生成文件中私有成员。此值是 *.g.dart,其中星号表示源文件名。
part 'user.g.dart';

/// 用于代码生成器知道需要为该类生成 JSON 序列化逻辑的注释。
@JsonSerializable()
class User {
  User(this.name, this.email);

  String name;
  String email;

  /// 用于根据映射创建新的 User 实例的必需工厂构造函数。将映射传递给生成的 `_$UserFromJson()` 构造函数。
  /// 构造函数以源类命名,在本例中为 User。
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

  /// `toJson` 是类声明支持序列化到 JSON 的约定。实现只是调用私有的、生成的帮助程序方法 `_$UserToJson`
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

通过此设置,源代码生成器会生成用于从 JSON 编码和解码 nameemail 字段的代码。

如果需要,也可以轻松自定义命名策略。例如,如果 API 返回使用 snake_case 的对象,而您想在模型中使用 lowerCamelCase

您可以使用带有 name 参数的 @JsonKey 注解:

dart
///告诉 json_serializable 将 "registration_date_millis" 映射到此属性。
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;

服务器和客户端最好遵循相同的命名策略。@JsonSerializable() 提供 fieldRename 枚举用于将 Dart 字段完全转换为 JSON 密钥。

修改 @JsonSerializable(fieldRename: FieldRename.snake) 等效于为每个字段添加 @JsonKey(name: '<snake_case>')

有时服务器数据是不确定的,因此有必要在客户端验证和保护数据。其他常用的 @JsonKey 注解包括:

dart
///如果 JSON 不包含此密钥或值为 `null`,则告诉 json_serializable 使用 "defaultValue"。
@JsonKey(defaultValue: false)
final bool isAdult;

///当为 `true` 时,告诉 json_serializable JSON 必须包含此密钥,如果密钥不存在,则抛出异常。
@JsonKey(required: true)
final String id;

///当为 `true` 时,告诉 json_serializable 生成的代码应该完全忽略此字段。
@JsonKey(ignore: true)
final String verificationCode;

运行代码生成工具

#

第一次创建 json_serializable 类时,您会遇到类似于下图所示的错误。

模型类生成的代码尚不存在时的 IDE 警告

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

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

一次性代码生成

#

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

虽然这很方便,但最好不必每次在模型类中进行更改时都手动运行构建。

持续生成代码

#

监视器 使我们的源代码生成过程更方便。它监视项目文件的更改,并在需要时自动构建必要的文件。通过在项目根目录中运行 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: trueUser 类现在如下所示:

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

更多参考

#

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