JSON转Dart
从JSON生成带fromJson和toJson的Dart类
JSON输入
Dart输出
什么是JSON转Dart转换?
JSON转Dart转换将原始JSON对象转换为带有类型化字段、命名构造函数、fromJson工厂方法和toJson方法的Dart类定义。Flutter中的Dart不支持运行时反射(dart:mirrors已被禁用),因此若不编写显式映射代码,就无法将JSON反序列化为类型化对象。每一个REST API响应、Firebase文档或配置数据,在能够以类型安全的方式访问其字段之前,都需要一个对应的Dart模型类。
典型的Dart JSON模型类会为每个键声明final字段,构造函数使用命名参数(非可空字段使用required关键字),工厂构造函数fromJson从Map<String, dynamic>中读取数据,toJson方法返回Map<String, dynamic>。嵌套的JSON对象会成为独立的类,数组会成为类型化的List字段,可空的JSON值在类型后使用Dart的空安全语法添加?后缀。
手动编写这些模型类意味着需要逐一读取每个JSON键、确定Dart类型、为每个字段编写fromJson类型转换(包括使用.map().toList()的列表映射)、构建toJson映射字面量,并对每个嵌套对象重复上述过程。一个包含12个字段和2个嵌套对象的JSON对象需要3个类、6行工厂方法和数十个类型转换表达式。转换工具只需一次粘贴,即可在毫秒内生成所有代码。
为什么使用JSON转Dart转换工具?
手动编写Dart模型类需要读取字段名、根据示例值推断类型、编写带有正确空值处理的fromJson转换,并对嵌套对象重复此过程。当API结构发生变化时,每次字段更新都需要同时修改构造函数、fromJson和toJson。转换工具可以消除这些重复性工作。
JSON转Dart使用场景
JSON到Dart的类型映射
每种JSON值都映射到特定的Dart类型。下表展示了转换工具如何翻译各种JSON类型,备选列显示在较少见或手动映射场景中使用的类型。
| JSON类型 | 示例 | Dart类型 | 备选类型 |
|---|---|---|---|
| string | "hello" | String | String |
| number (integer) | 42 | int | int |
| number (float) | 3.14 | double | double |
| boolean | true | bool | bool |
| null | null | dynamic | Null / dynamic |
| object | {"k": "v"} | NestedClass | Map<String, dynamic> |
| array of strings | ["a", "b"] | List<String> | List<String> |
| array of objects | [{"id": 1}] | List<Item> | List<Item> |
| mixed array | [1, "a"] | List<dynamic> | List<dynamic> |
Dart JSON序列化方案
Dart和Flutter提供多种处理JSON序列化的方式。手动fromJson/toJson是最简单的方案,不需要代码生成。对于较大的项目,基于build_runner的方案(如json_serializable和freezed)可以在编译时生成模板代码,减少错误并保持模型与JSON契约的一致性。
| 方案 | 说明 | 来源 |
|---|---|---|
| json_serializable | Code-generation-based JSON serialization. Generates .g.dart files at build time via build_runner. Type-safe and compile-time verified. | Official (Google) |
| freezed | Generates immutable data classes with copyWith, fromJson/toJson, equality, and pattern matching support. Built on top of json_serializable. | Community (rrousselGit) |
| built_value | Generates immutable value types with serialization. Enforces immutability patterns. Used in larger codebases with strict data modeling. | |
| dart_mappable | Annotation-based mapper that generates fromJson, toJson, copyWith, and equality. Simpler setup than freezed with similar features. | Community |
| Manual fromJson/toJson | Hand-written factory constructors and toJson methods. No code generation needed. Full control over the mapping logic. | Built-in Dart |
手动方式 vs json_serializable vs freezed
Dart有三种常见的JSON模型类处理方案。手动fromJson/toJson无需任何依赖;json_serializable自动化映射代码;freezed在json_serializable的基础上增加了不可变性、copyWith和模式匹配。
代码示例
以下示例展示如何使用生成的Dart类进行JSON反序列化、如何配合build_runner使用json_serializable,以及如何通过JavaScript和Python以编程方式生成Dart类。
import 'dart:convert';
class User {
final int id;
final String name;
final String email;
final bool active;
final Address address;
final List<String> tags;
User({
required this.id,
required this.name,
required this.email,
required this.active,
required this.address,
required this.tags,
});
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
active: json['active'] as bool,
address: Address.fromJson(json['address'] as Map<String, dynamic>),
tags: (json['tags'] as List<dynamic>).map((e) => e as String).toList(),
);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
'active': active,
'address': address.toJson(),
'tags': tags,
};
}
class Address {
final String street;
final String city;
final String zip;
Address({required this.street, required this.city, required this.zip});
factory Address.fromJson(Map<String, dynamic> json) => Address(
street: json['street'] as String,
city: json['city'] as String,
zip: json['zip'] as String,
);
Map<String, dynamic> toJson() => {'street': street, 'city': city, 'zip': zip};
}
void main() {
final jsonStr = '{"id":1,"name":"Alice","email":"alice@example.com","active":true,"address":{"street":"123 Main","city":"Springfield","zip":"12345"},"tags":["admin","user"]}';
final user = User.fromJson(jsonDecode(jsonStr));
print(user.name); // -> Alice
print(jsonEncode(user.toJson())); // -> round-trip back to JSON
}// pubspec.yaml dependencies:
// json_annotation: ^4.8.0
// dev_dependencies:
// build_runner: ^2.4.0
// json_serializable: ^6.7.0
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart'; // generated by: dart run build_runner build
@JsonSerializable()
class User {
final int id;
final String name;
final String email;
@JsonKey(name: 'is_active')
final bool isActive;
final List<String> tags;
User({
required this.id,
required this.name,
required this.email,
required this.isActive,
required this.tags,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
// Run code generation:
// dart run build_runner build --delete-conflicting-outputsfunction jsonToDart(obj, name = "Root") {
const classes = [];
function infer(val, fieldName) {
if (val === null) return "dynamic";
if (typeof val === "string") return "String";
if (typeof val === "number") return Number.isInteger(val) ? "int" : "double";
if (typeof val === "boolean") return "bool";
if (Array.isArray(val)) {
const first = val.find(v => v !== null);
if (!first) return "List<dynamic>";
return `List<${infer(first, fieldName)}>`;
}
if (typeof val === "object") {
const cls = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
build(val, cls);
return cls;
}
return "dynamic";
}
function build(obj, cls) {
const fields = Object.entries(obj).map(([k, v]) =>
` final ${infer(v, k)} ${k};`
);
classes.push(`class ${cls} {\n${fields.join("\n")}\n}`);
}
build(obj, name);
return classes.join("\n\n");
}
console.log(jsonToDart({ id: 1, name: "Alice", scores: [98, 85] }, "User"));
// class User {
// final int id;
// final String name;
// final List<int> scores;
// }import json
def json_to_dart(obj: dict, class_name: str = "Root") -> str:
classes = []
def infer(val, name):
if val is None:
return "dynamic"
if isinstance(val, bool):
return "bool"
if isinstance(val, int):
return "int"
if isinstance(val, float):
return "double"
if isinstance(val, str):
return "String"
if isinstance(val, list):
if not val:
return "List<dynamic>"
return f"List<{infer(val[0], name)}>"
if isinstance(val, dict):
cls = name[0].upper() + name[1:]
build(val, cls)
return cls
return "dynamic"
def build(obj, cls):
fields = [f" final {infer(v, k)} {k};" for k, v in obj.items()]
classes.append(f"class {cls} {{\n" + "\n".join(fields) + "\n}")
build(obj, class_name)
return "\n\n".join(classes)
data = json.loads('{"id": 1, "name": "Alice", "active": true}')
print(json_to_dart(data, "User"))
# class User {
# final int id;
# final String name;
# final bool active;
# }