Json转dart类
简介
实战中,后台接口往往返回一些结构化的数据,如jSON、XML、String等,为了方便在代码中操作JSON,通常会先将JSON格式的字符串转化为Dart对象,这个可以通过dart:convert中内置的JSON解码器json.decode()来实现,该方法可以根据JSON字符串具体内容,将其转化为List或Mao,这样就可以通过它们来查找所需的值:
//一个JSON格式的用户列表字符串
String jsonStr='[{"name":"Jack"},{"name":"Rose"}]';`
//将JSON字符串转为Dart对象(此处是List)
List items=json.decode(jsonStr);
//输出第一个用户的姓名
print(items[0]["name"]);
通过json.decode()将JSON字符串转为List/Map的方法比较简单,它没有外部依赖或其他的设置,对于小项目很方便。但当项目变大时,这种手动便携序列化逻辑可能变得难以管理且容易出错,如:
{
"name": "John Smith",
"email": "john@example.com"
}
可以通过调用json.decode方法来解码JSON,使用JSON字符串作为参数:
Map<String, dynamic> user = json.decode(json);
print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');
由于json.decode()仅返回一个Map<String,dynamic>,这意味着指导运行时才知道值的类型。通过这种方法,会失去大部分静态类型语言特性:类型安全、自动补全和最重要的编译时异常,这样一来,我们的代码会变得非常容易出错。例如当访问name或email字段时,输入错误。由于这个json在map结构中,所以编译器不知道这个错误的字段名,所以编译时不会报错。
想要规避这类问题,好的解决方案是“json model化”,具体的做法就是,通过与定义一些与json结构对应的Model类,然后在请求到数据后再动态根据数据创建出Model类的实例。这样,在开发阶段使用的是Model类的实例,而不是Map/List,这样访问内部属性时就不会拼写错误。例如,可以通过引入一个简单的模型类(Model class)User类。User类内部有:
- 一个User.fromJson构造函数,用于从map构造出一个User实例map结构。
- 一个toJson方法,用于将user实例转化为一个map。
这样,调用代码现在可以具有类型安全、自动补全字段、以及编译时异常。
举例:
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() =>
<String, dynamic>{
'name': name,
'email': email,
};
}
序列化逻辑转移到了模型内部。采用这种方法,可以非常容易的反序列化Model。
Map userMap = json.decode(json);
var user = User.fromJson(userMap);
print('Howdy, ${user.name}!');
print('We sent the verification link to ${user.email}.');
要序列化一个user,只需将user对象传递给json.encode方法。不需要手动的调用toJson,因为JSON.encode内部会自动调用。
String json = json.encode(user);
这样调用代码就不用担心JSON序列化,但是Model类还是必须要自己实现。
在实际场景中,JSON对象很少会单层,嵌套的JSON对象很常见。
自动生成Model
官方推荐json_serializable package包。它是一个自动化的源码生成器,可以在开发阶段生成JSON序列化模版,由于序列化代码不再由手写和维护,将运行时产生JSON序列化异常的风险降至最低。
- 在项目中添加
dependencies:
json_annotation: ^4.8.0
dev_dependencies:
build_runner: ^2.3.3
json_serializable: 6.6.1
然后Pub get 2. 用json_serializable的方式创建Model类 user.dart
import 'package:json_annotation/json_annotation.dart';
// user.g.dart 将在我们运行生成命令后自动生成
part 'user.g.dart';
///这个标注是告诉生成器,这个类是需要生成Model类的
@JsonSerializable()
class User{
User(this.name, this.email);
String name;
String email;
//不同的类使用不同的mixin即可
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
有了上面的设置,源码生成器将生成于序列化name和email字段的JSON代码。
如果需要,自定义命名策略,可以使用@JsonKey标注
//显式关联JSON字段名与Model属性的对应关系
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;
- 运行代码生成程序 json_serializable第一次创建类是,会报错。这是因为Model类的代码还未生成,需要运行代码生成器生成序列化模版。
- 一次性生成
flutter packages pub run build_runner build
在需要时为我们的Model生成json序列化代码,它通过源文件找出需要生成Model类的源文件(包含@JsonSerializable标注)来生成对应的.g.dart文件。一个好的建议是将所有Model类放在一个单独的目录下,然后在该目录下执行命令。
- 持续生成 使用_watcher_可以使我们的源码生成过程更加方便,它会监视项目中文件的变化,并在需要时自动构建必要的文件。可以通过flutter packages pub run build_runner watch 在项目根目录下运行来启动_watcher_。只需要启动一次观察器,它就会在后台运行。
一句命令实现JSON转dart类
上面的方法最大的问题就是要为每一个json写模版,比较耗时和枯燥。 这里介绍一下大佬用dart实现的一个脚本,可以自动生成模版,并直接将json转为Model类。
- 定义一个模板的模板,名为template.dart
import 'package:json_annotation/json_annotation.dart';
%t
part '%s.g.dart';
@JsonSerializable()
class %s {
%s();
%s
factory %s.fromJson(Map<String,dynamic> json) => _$%sFromJson(json);
Map<String, dynamic> toJson() => _$%sToJson(this);
}
模板中的%t/%s为占位符,将在脚本运行时动态的被替换为合适的导入头和类名。 2. 写一个自动生成模板的脚本(mo.dart),它可以根据指定的json目录,遍历生成模板,生成时定义规则:
- 如果JSON文件名以下划线"_"开头,则忽略JSON文件
- 如果复杂的JSON对象通过特殊的标志来手动指定嵌套的对象。
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
const TAG = "$";
const SRC = "./json"; //JSON 目录
const DIST = "lib/models/"; //输出model目录
void walk() {
//遍历JSON目录生成模板
var src = Directory(SRC);
var list = src.listSync();
var template = File("./template.dart").readAsStringSync();
File file;
list.forEach((f) {
if (FileSystemEntity.isFileSync(f.path)) {
file = File(f.path);
var paths = path.basename(f.path).split(".");
String name = paths.first;
if (paths.last.toLowerCase() != "json" || name.startsWith("_")) return;
if (name.startsWith("_")) return;
//下面生成模板
var map = json.decode(file.readAsStringSync());
//为了避免重复导入相同的包,我们用Set来保存生成的import语句。
var set = Set<String>();
StringBuffer attrs = StringBuffer();
(map as Map<String, dynamic>).forEach((key, v) {
if (key.startsWith("_")) return;
//所有字段都定义为可空
attrs.write(getType(v, set, name)+"?");
attrs.write(" ");
attrs.write(key);
attrs.writeln(";");
attrs.write(" ");
});
String className = name[0].toUpperCase() + name.substring(1);
var dist = format(template, [
name,
className,
className,
attrs.toString(),
className,
className,
className
]);
var _import = set.join(";\r\n");
_import += _import.isEmpty ? "" : ";";
dist = dist.replaceFirst("%t", _import);
//将生成的模板输出
File("$DIST$name.dart").writeAsStringSync(dist);
}
});
}
String changeFirstChar(String str, [bool upper = true]) {
return (upper ? str[0].toUpperCase() : str[0].toLowerCase()) +
str.substring(1);
}
//将JSON类型转为对应的dart类型
String getType(v, Set<String> set, String current) {
current = current.toLowerCase();
if (v is bool) {
return "bool";
} else if (v is num) {
return "num";
} else if (v is Map) {
return "Map<String,dynamic>";
} else if (v is List) {
return "List";
} else if (v is String) {
//处理特殊标志
if (v.startsWith("$TAG[]")) {
var className = changeFirstChar(v.substring(3), false);
if (className.toLowerCase() != current) {
set.add('import "$className.dart"');
}
return "List<${changeFirstChar(className)}>";
} else if (v.startsWith(TAG)) {
var fileName = changeFirstChar(v.substring(1), false);
if (fileName.toLowerCase() != current) {
set.add('import "$fileName.dart"');
}
return changeFirstChar(fileName);
}
return "String";
} else {
return "String";
}
}
//替换模板占位符
String format(String fmt, List<Object> params) {
int matchIndex = 0;
String replace(Match m) {
if (matchIndex < params.length) {
switch (m[0]) {
case "%s":
return params[matchIndex++].toString();
}
} else {
throw Exception("Missing parameter for string format");
}
throw Exception("Invalid format string: " + m[0].toString());
}
return fmt.replaceAllMapped("%s", replace);
}
void main() {
walk();
}
- 写一个shell(mo.sh),将模板和生成model串起来:
dart mo.dart
flutter packages pub run build_runner build --delete-conflicting-outputs
至此,脚本完成,在根目录下新建一个json目录,然后把user.json移进去,然后在lib目录下创建一个models目录,用于保存最终生成的Model类。执行命令即可生成Model类:
./mo.sh
运行后,一切都将自动执行。
嵌套的JSON处理
创建一个person.json文件:
{
"name": "John Smith",
"email": "john@example.com",
"mother":{
"name": "Alice",
"email":"alice@example.com"
},
"friends":[
{
"name": "Jack",
"email":"Jack@example.com"
},
{
"name": "Nancy",
"email":"Nancy@example.com"
}
]
}
期望生成的Model:
import 'package:json_annotation/json_annotation.dart';
part 'person.g.dart';
@JsonSerializable()
class Person {
Person();
String? name;
String? email;
Person? mother;
List<Person>? friends;
factory Person.fromJson(Map<String,dynamic> json) => _$PersonFromJson(json);
Map<String, dynamic> toJson() => _$PersonToJson(this);
}
修改JSON,添加一些特殊的标志,重新运行mo.sh
{
"name": "John Smith",
"email": "john@example.com",
"mother":"$person",
"friends":"$[]person"
}
使用美元符“$”作为特殊标志符(如果与内容冲突,可以修改mo.dart中的TAG常量,自定义标志符),脚本在遇到特殊标志符后会先把相应字段转为相应的对象或对象数组,对象数组需要在标志符后面添加数组符“[]”,符号后面接具体的类型名,此例中是person。其他类型同理,给User添加一个Person类型的 boss字段:
{
"name": "John Smith",
"email": "john@example.com",
"boss":"$person"
}
重新运行mo.sh,生成user.dart
import 'package:json_annotation/json_annotation.dart';
import "person.dart";
part 'user.g.dart';
@JsonSerializable()
class User {
User();
String? name;
String? email;
Person? boss;
factory User.fromJson(Map<String,dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
Json_model包
大佬写的一个比较完善的Json_model包,具备灵活的配置和自定义功能,加入开发依赖后,可以一条命令,根据json文件生成Dart类:
JSON文件:
{
"@meta": { // @meta 可以定制单个 json 的生成规则,默认使用全局配置
"import": [
"test_dir/profile.dart" // 导入其他文件
],
"comments": {
"name": "名字" // 给 "name" 字段添加注释
},
"nullable": false, // 字段默认非可空,会生成 late
"ignore": false // 是否跳过当前 JSON 的 model 类生成
},
"@JsonKey(ignore: true) Profile?": "profile",
"@JsonKey(name: '+1') int?": "loved",
"name": "wendux",
"father": "$user",
"friends": "$[]user",
"keywords": "$[]String",
"age?": 20 // 指定 age 字段可空
}
生成模型:
import 'package:json_annotation/json_annotation.dart';
import 'test_dir/profile.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
User();
@JsonKey(ignore: true) Profile? profile;
@JsonKey(name: '+1') int? loved;
//名字
late String name;
late User father;
late List<User> friends;
late List<String> keywords;
num? age;
factory User.fromJson(Map<String,dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
优劣
- Json_model需要单独维护一个存放Json文件的文件夹,如果有改动,只需改Json文件便可更新生成Model类;IDE差距一半需要用户手动将Json内容拷贝复制到一个输入框中,生成之后Json文件没有存档的话,之后要改动就需要手动。
- Json_model可以手动置顶某个字段引用其他Model类,可以避免生成重复的类;IDE插件一般会为每一个json文件中所有嵌套对象单独生成一个Model类,即使这些嵌套对象可能在其他Model中已经生成过。
- Json_model提供命令行转化方式,可以方便集成到CI等非UI环境的场景。
总结:Flutter中没有像iOS中MJExtension或JSONModel等反射序列化包,因为Flutter中是禁用反射机制的。运行时反射会感染Dart的tree shaking。