对于需要从互联网获取数据的应用程序来说,解析JSON是一项非常常见的任务。
而根据你需要处理的JSON数据的多少,你有两个选择。
- 手动编写所有的JSON解析代码
- 用代码生成来自动处理
本指南将重点介绍如何手动解析JSON到Dart代码,包括:
- 编码和解码JSON
- 定义类型安全的模型类
- 使用工厂构造器将JSON解析为Dart代码
- 处理空值/可选值
- 数据验证
- 序列化回JSON
- 解析复杂/嵌套的JSON数据
- 使用deep_pick包挑选深度值
在本文结束时,你将学会如何用强大的JSON解析和验证代码编写模型类。
我们有很多内容要讲,所以让我们从基础开始。
剖析JSON文档
如果你以前使用过任何REST API,这个JSON响应样本应该看起来很熟悉。
{
"name": "Pizza da Mario",
"cuisine": "Italian",
"reviews": [
{
"score": 4.5,
"review": "The pizza was amazing!"
},
{
"score": 5.0,
"review": "Very friendly staff, excellent service!"
}
]
}
这个简单的文档表示一个键值对的映射,其中:
- 键是字符串
- 值可以是任何原始类型(如布尔值、数字或字符串),或一个集合(如列表或地图)
JSON数据可以包含键值对的映射(使用
{})和列表(使用[])。这些都可以被组合起来,创建代表复杂数据结构的嵌套集合。
事实上,我们的例子包括一个特定餐厅的评论列表。
"reviews": [
{
"score": 4.5,
"review": "The pizza was amazing!"
},
{
"score": 5.0,
"review": "Very friendly staff, excellent service!"
}
]
这些被存储为reviews 关键的映射列表,每个评论本身就是一个有效的JSON片段。
编码和解码JSON
当JSON响应通过网络发送时,整个有效载荷被编码为一个字符串。
但在我们的Flutter应用程序中,我们不想手动提取字符串中的数据。
// json payload as a string
final json = '{ "name": "Pizza da Mario", "cuisine": "Italian", "reviews": [{"score": 4.5,"review": "The pizza was amazing!"},{"score": 5.0,"review": "Very friendly staff, excellent service!"}]}';
相反,我们可以通过对JSON进行解码来读取其内容。
要在网络上发送JSON数据,首先需要对它进行编码或序列化。编码是将数据结构变成字符串的过程。相反的过程被称为解码或反序列化。当你收到作为字符串的JSON有效载荷时,你需要在使用它之前对其进行解码或反序列化。
用dart:convert对JSON进行解码
为了简单起见,让我们考虑这个小的JSON有效载荷。
// this represents some response data we get from the network, for example:
// ```
// final response = await http.get(uri);
// final jsonData = response.body
// ```
final jsonData = '{ "name": "Pizza da Mario", "cuisine": "Italian" }';
为了读取其中的键和值,我们首先需要使用dart:convert 包对其进行解码。
// 1. import dart:convert
import 'dart:convert';
// this represents some response data we get from the network
final jsonData = '{ "name": "Pizza da Mario", "cuisine": "Italian" }';
// 2. decode the json
final parsedJson = jsonDecode(jsonData);
// 3. print the type and value
print('${parsedJson.runtimeType} : $parsedJson');
如果我们运行这段代码,我们会得到这样的输出。
_InternalLinkedHashMap<String, dynamic> : {name: Pizza da Mario, cuisine: Italian}
实际上,结果类型与Map<String, dynamic> 相同。
_InternalLinkedHashMap是**LinkedHashMap的一个私有实现,它又实现了Map**。
所以键的类型是String ,值的类型是dynamic 。这是有道理的,因为每个JSON值可能是一个原始类型(布尔/数字/字符串),或者是一个集合(列表或地图)。
事实上,jsonDecode() 是一个通用的方法,对任何有效的JSON有效载荷都有效,不管它里面是什么。它所做的就是对其进行解码并返回一个dynamic 。
但是如果我们在Dart中使用dynamic 值,我们就会失去强类型安全的所有好处。一个更好的方法是定义一些自定义的模型类,在个案的基础上表示我们的响应数据。
由于Dart是一种静态类型的语言,重要的是将JSON数据转换为代表现实世界对象(如食谱、雇员等)的模型类,并充分利用类型系统。
因此,让我们看看如何做到这一点。
将JSON解析为Dart模型类
鉴于这个简单的JSON。
{
"name": "Pizza da Mario",
"cuisine": "Italian"
}
我们可以写一个Restaurant 类来表示它。
class Restaurant {
Restaurant({required this.name, required this.cuisine});
final String name;
final String cuisine;
}
因此,与其像这样读数据,不如像这样读。
parsedJson['name']; // dynamic
parsedJson['cuisine']; // dynamic
我们可以像这样读取它。
restaurant.name; // guaranteed to be a non-nullable, immutable String
restaurant.cuisine; // guaranteed to be a non-nullable, immutable String
这样就干净多了,我们可以利用类型系统来获得编译时的安全性,避免错别字和其他错误。
然而,我们还没有指定如何将我们的parsedJson 转化为Restaurant 对象!
JSON到Dart。添加一个工厂构造函数
让我们定义一个工厂构造函数来处理这个问题。
factory Restaurant.fromJson(Map<String, dynamic> data) {
// note the explicit cast to String
// this is required if robust lint rules are enabled
final name = data['name'] as String;
final cuisine = data['cuisine'] as String;
return Restaurant(name: name, cuisine: cuisine);
}
对于JSON解析来说,工厂构造函数是一个不错的选择,因为它可以让我们在返回结果之前做一些工作(创建变量,执行一些验证)。这在普通的(生成式)构造函数中是不可能的。
由于我们的map的值是
dynamic,所以我们明确地将它们转换为我们想要的类型(本例中是String)。这是一个很好的做法,可以通过使用推荐的lint规则来强制执行。
这就是我们如何使用我们的构造函数。
// type: String
final jsonData = '{ "name": "Pizza da Mario", "cuisine": "Italian" }';
// type: dynamic (runtime type: _InternalLinkedHashMap<String, dynamic>)
final parsedJson = jsonDecode(jsonData);
// type: Restaurant
final restaurant = Restaurant.fromJson(parsedJson);
好多了。现在我们其余的代码可以使用Restaurant ,并获得Dart中强类型安全的所有优势。
JSON到Dart的空值安全
有时我们需要解析一些JSON,它可能有也可能没有某个键值对。
例如,假设我们有一个可选字段,告诉我们一家餐馆是什么时候开业的。
{
"name": "Ezo Sushi",
"cuisine": "Japanese",
"year_opened": 1990
}
如果year_opened 字段是可选的,我们可以在我们的模型类中用一个可空变量来表示它。
下面是Restaurant 类的一个更新的实现。
class Restaurant {
Restaurant({required this.name, required this.cuisine, this.yearOpened});
final String name; // non-nullable
final String cuisine; // non-nullable
final int? yearOpened; // nullable
factory Restaurant.fromJson(Map<String, dynamic> data) {
final name = data['name'] as String; // cast as non-nullable String
final cuisine = data['cuisine'] as String; // cast as non-nullable String
final yearOpened = data['year_opened'] as int?; // cast as nullable int
return Restaurant(name: name, cuisine: cuisine, yearOpened: yearOpened);
}
}
一般来说,我们应该将可选的JSON值映射为可忽略的Dart属性。另外,我们也可以使用非空值的Dart属性,并有一个合理的默认值,就像本例中一样。
// note: all the previous properties have been omitted for simplicity
class Restaurant {
Restaurant({
// 1. required
required this.hasIndoorSeating,
});
// 2. *non-nullable*
final bool hasIndoorSeating;
factory Restaurant.fromJson(Map<String, dynamic> data) {
// 3. cast as *nullable* bool
final hasIndoorSeating = data['has_indoor_seating'] as bool?;
return Restaurant(
// 4. use ?? operator to provide a default value
hasIndoorSeating: hasIndoorSeating ?? true,
);
}
}
请注意,在这种情况下,我们使用空值凝聚运算符(??)来提供一个默认值。
数据验证
使用工厂构造函数的一个好处是,如果需要,我们可以做一些额外的验证。
例如,我们可以写一些防御性的代码,如果缺少一个必需的值,就抛出一个UnsupportedError 。
factory Restaurant.fromJson(Map<String, dynamic> data) {
// casting as a nullable String so we can do an explicit null check
final name = data['name'] as String?;
if (name == null) {
throw UnsupportedError('Invalid data: $data -> "name" is missing');
}
// casting as a nullable String so we can do an explicit null check
final cuisine = data['cuisine'] as String?;
if (cuisine == null) {
throw UnsupportedError('Invalid data: $data -> "cuisine" is missing');
}
final yearOpened = data['year_opened'] as int?;
// thanks to the if statements above, name and cuisine are guaranteed to be non-null here
return Restaurant(name: name, cuisine: cuisine, yearOpened: yearOpened);
}
一般来说,作为API的消费者,我们的工作是为每个值计算出来。
- 它的类型 (
String,int, 等等。) - 它是否是可选的****(nullablevsnon-nullable)。
- 允许的数值范围是多少
这将使我们的JSON解析代码更加强大。而且我们将不必在我们的部件类中处理无效的数据,因为所有的验证都是预先完成的。
为了使你的代码可以投入生产,考虑编写单元测试来测试你所有模型类的所有可能的边缘情况。
用toJson()进行JSON序列化
解析JSON是很有用的,但有时我们想把一个模型对象转换回JSON并通过网络发送。
要做到这一点,我们可以为我们的Restaurant 类定义一个toJson() 方法。
// note the return type
Map<String, dynamic> toJson() {
// return a map literal with all the non-null key-value pairs
return {
'name': name,
'cuisine': cuisine,
// here we use collection-if to account for null values
if (yearOpened != null) 'year_opened': yearOpened,
};
}
而我们可以像这样使用。
// given a Restaurant object
final restaurant = Restaurant(name: "Patatas Bravas", cuisine: "Spanish");
// convert it to map
final jsonMap = restaurant.toJson();
// encode it to a JSON string
final encodedJson = jsonEncode(jsonMap);
// then send it as a request body with any networking package
解析嵌套的JSON:地图的列表
现在我们了解了JSON解析和验证的基本知识,让我们回到最初的例子,看看如何解析它。
{
"name": "Pizza da Mario",
"cuisine": "Italian",
"reviews": [
{
"score": 4.5,
"review": "The pizza was amazing!"
},
{
"score": 5.0,
"review": "Very friendly staff, excellent service!"
}
]
}
我们想一直使用模型类和类型安全,所以让我们定义一个Review 类。
class Review {
Review({required this.score, this.review});
// non-nullable - assuming the score field is always present
final double score;
// nullable - assuming the review field is optional
final String? review;
factory Review.fromJson(Map<String, dynamic> data) {
final score = data['score'] as double;
final review = data['review'] as String?;
return Review(score: score, review: review);
}
Map<String, dynamic> toJson() {
return {
'score': score,
// here we use collection-if to account for null values
if (review != null) 'review': review,
};
}
}
然后我们可以更新Restaurant 类,以包括一个评论的列表。
class Restaurant {
Restaurant({
required this.name,
required this.cuisine,
this.yearOpened,
required this.reviews,
});
final String name;
final String cuisine;
final int? yearOpened;
final List<Review> reviews;
}
我们还可以更新工厂构造函数。
factory Restaurant.fromJson(Map<String, dynamic> data) {
final name = data['name'] as String;
final cuisine = data['cuisine'] as String;
final yearOpened = data['year_opened'] as int?;
// cast to a nullable list as the reviews may be missing
final reviewsData = data['reviews'] as List<dynamic>?;
// if the reviews are not missing
final reviews = reviewsData != null
// map each review to a Review object
? reviewsData.map((reviewData) => Review.fromJson(reviewData))
// map() returns an Iterable so we convert it to a List
.toList()
// use an empty list as fallback value
: <Review>[];
// return result passing all the arguments
return Restaurant(
name: name,
cuisine: cuisine,
yearOpened: yearOpened,
reviews: reviews,
);
}
下面是所有新代码的分类。
- 评论可能会丢失,因此我们将其转换为nullable。
List - 列表中的值可以有任何类型,所以我们使用
List<dynamic> - 我们使用
.map()操作符将每个dynamic值转换为Review对象。Review.fromJson() - 如果评论缺失,我们使用一个空列表(
<Review>[])作为备用。
这个具体的实现对什么可能是或不可能是
null,使用什么后备值等做了一些假设。你需要编写最适合你使用情况的解析代码。
嵌套模型的序列化
作为最后一步,这里是toJson() 方法,将一个Restaurant (和它的所有评论)转换回一个Map 。
Map<String, dynamic> toJson() {
return {
'name': name,
'cuisine': cuisine,
if (yearOpened != null) 'year_opened': yearOpened,
'reviews': reviews.map((review) => review.toJson()).toList(),
};
}
注意我们是如何将List<Review> 转换回List<Map<String, dynamic>> ,因为我们需要将所有的嵌套值也序列化(而不仅仅是Restaurant 类本身)。
通过上面的代码,我们可以创建一个Restaurant 对象,并将其转换回一个可以被编码和打印或通过网络发送的地图。
final restaurant = Restaurant(
name: 'Pizza da Mario',
cuisine: 'Italian',
reviews: [
Review(score: 4.5, review: 'The pizza was amazing!'),
Review(score: 5.0, review: 'Very friendly staff, excellent service!'),
],
);
final encoded = jsonEncode(restaurant.toJson());
print(encoded);
// output: {"name":"Pizza da Mario","cuisine":"Italian","reviews":[{"score":4.5,"review":"The pizza was amazing!"},{"score":5.0,"review":"Very friendly staff, excellent service!"}]}
挑选深度值
将整个JSON文档解析为类型安全的模型类是一个非常常见的用例。
但有时我们只想读取一些可能是深度嵌套的特定值。
让我们再一次考虑我们的JSON样本。
{
"name": "Pizza da Mario",
"cuisine": "Italian",
"reviews": [
{
"score": 4.5,
"review": "The pizza was amazing!"
},
{
"score": 5.0,
"review": "Very friendly staff, excellent service!"
}
]
}
如果我们想得到第一次审查的分数,我们可以这样做。
final decodedJson = jsonDecode(jsonData); // dynamic
final score = decodedJson['reviews'][0]['score'] as double;
这是有效的Dart代码,因为decodedJson 变量是dynamic ,我们可以用下标运算符([])。
但是上面的代码既不是null安全的,也不是类型安全的,我们必须明确地将解析后的值转换为我们想要的类型 (double)。
我们怎样才能改进这一点呢?
deep_pick包
deep_pick包通过一个类型安全的API简化了JSON解析。
一旦安装,我们可以使用它来获得我们想要的值,而不需要手动转换。
import 'dart:convert';
import 'package:deep_pick/deep_pick.dart';
final decodedJson = jsonDecode(jsonData); // dynamic
final score = pick(decodedJson, 'reviews', 0, 'score').asDoubleOrThrow();
deep_pick提供了各种灵活的API,我们可以用它来解析原始类型、列表、地图、DateTime对象,以及更多。阅读文档获取更多信息。
奖励:添加一个toString()方法
当使用模型类时,提供一个toString() 方法是非常有用的,这样它们就可以很容易地被打印到控制台。
由于我们已经有了一个toJson() 方法,我们可以像这样利用它。
@override
String toString() => toJson().toString();
结果是,我们可以像这样直接打印我们的餐厅。
print(restaurant);
// output: {name: Pizza da Mario, cuisine: Italian, reviews: [{score: 4.5, review: The pizza was amazing!}, {score: 5.0, review: Very friendly staff, excellent service!}]}
如果我们能够使用
==操作符来比较我们的模型类,那就更好了,这在编写单元测试时经常需要。要了解如何做到这一点,请查看Equatable包。
关于性能的注意事项
当你解析小的JSON文档时,你的应用程序可能会保持响应,不会遇到性能问题。
但是解析非常大的JSON文档可能会导致昂贵的计算,最好在后台单独的Dart隔离区中进行。官方文档对此有一个很好的指导。
总结
JSON序列化是一项非常平凡的工作。但如果我们想让我们的应用程序正常工作,那么正确地完成它并注意细节是非常重要的。
- 使用
jsonEncode()和jsonDecode()从'dart:convert'来序列化JSON数据 - 用
fromJson()和toJson()为你应用程序中所有特定领域的JSON对象创建模型类 - 在
fromJson()内添加明确的转换、验证和空值检查,以使解析代码更加健壮 - 对于嵌套的JSON数据(地图的列表),应用
fromJson()和toJson()方法 - 考虑使用deep_pick包,以类型安全的方式解析JSON。
虽然我们用作参考的JSON例子并不太复杂,但我们最终还是得到了相当多的代码。
如果你有很多不同的模型类,或者每个类都有很多属性,那么用手写所有的解析代码就会变得很费时而且容易出错。
在这种情况下,代码生成是一个更好的选择,这篇文章解释了如何使用它。
而如果你需要解析大的JSON数据,你应该在一个单独的隔离区中进行,以获得最佳性能。这篇文章涵盖了所有的细节。