DevToolBox免费
博客

JSON 转 Dart:Flutter 类生成完整指南(空安全)

11 分钟阅读作者 DevToolBox
TL;DR

将 JSON 转换为具有空安全的 Dart 类对于健壮的 Flutter 应用至关重要。使用我们的免费在线工具可即时生成,或在生产代码库中使用 json_serializable 包。始终区分 required 字段、可选字段(?) 和可为 null 的字段(Type?)。freezed 包添加了不可变性和 copyWith()。使用注解后需运行 `flutter pub run build_runner build`。

核心要点
  • Dart 空安全要求用 ? 标记可为 null 的字段,并对必填构造函数参数使用 required 关键字。
  • json_serializable + build_runner 消除了 fromJson/toJson 样板代码——用 @JsonSerializable() 注解并运行代码生成。
  • freezed 在 json_serializable 基础上创建不可变数据类,具有 copyWith()、== 重写和模式匹配功能。
  • 嵌套对象需要自己的 fromJson 工厂方法;对类型化列表使用 List<T>.from(json['key'].map((x) => T.fromJson(x)))。
  • DateTime 解析在 fromJson 中使用 DateTime.parse(),在 toJson 中使用 .toIso8601String()。
  • 对于枚举,使用 @JsonKey(unknownEnumValue: EnumType.unknown) 处理枚举定义中尚未包含的值。
  • 始终编写往返单元测试:expect(User.fromJson(user.toJson()), equals(user)) 以尽早发现序列化错误。
试用我们的免费 JSON 转 Dart 工具

为什么要将 JSON 转换为 Dart 类?

Flutter 应用使用 JSON 与 REST API、WebSocket 和本地存储进行通信。没有强类型的 Dart 类,你只能使用动态 Map——没有自动补全、没有编译时检查,运行时异常随时可能发生。Dart 的健全空安全使这一点更加重要:编译器强制正确处理可为 null 的值。

从 JSON 生成 Dart 模型类带来以下好处:

  • 编译时类型安全:访问不存在的属性,分析器会立即捕获。
  • IDE 自动补全:每个字段、方法和嵌套对象都可通过 IntelliSense 发现。
  • 空安全强制执行:Dart 在语言级别将可为 null (String?) 与不可为 null (String) 分离。
  • 重构信心:重命名类中的字段,分析器会标记所有需要更新的使用位置。
  • 可测试性:不可变值对象易于构建、比较和在单元测试中断言。

Dart 类剖析:fromJson 和 toJson

规范的 Dart 模型类有三个部分:字段(带空安全注解)、用于反序列化的工厂构造函数以及用于序列化的方法:

class User {
  final int id;
  final String name;
  final String? email;       // 可为 null
  final DateTime createdAt;

  const User({
    required this.id,
    required this.name,
    this.email,
    required this.createdAt,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String?,
      createdAt: DateTime.parse(json['created_at'] as String),
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
      'created_at': createdAt.toIso8601String(),
    };
  }
}

该类的关键设计决策:

  • final 字段使类在构造后实际上不可变。
  • required 在构造函数中强制调用者始终提供非 null 字段。
  • 工厂构造函数将每个 JSON 值转换为其 Dart 类型,将 snake_case 键转换为 camelCase 字段。
  • String?(可为 null 的 String)映射到可以为 null 或缺失的 JSON 值。

生成类中的空安全

Dart 2.12+ 强制执行健全空安全。这意味着类型系统跟踪哪些值可以为 null,哪些不能。从 JSON 生成类时,必须正确识别可为 null 与不可为 null 的字段:

// 非 null:字段始终存在且不为 null
final String name;           // json['name'] 始终是 String

// 可为 null:字段可能缺失或显式为 null
final String? bio;           // json['bio'] 可以为 null

// 空断言:你确定运行时值不为 null
final String admin = json['role']!;  // 若为 null 则抛出异常!

// 空合并:值为 null 时提供默认值
final int age = json['age'] ?? 0;    // 缺失时默认为 0

最佳实践:优先提供显式默认值,而不是使用空断言运算符(!)。断言可能在运行时抛出"对 null 值使用了空值检查运算符"异常。

factory UserProfile.fromJson(Map<String, dynamic> json) {
  return UserProfile(
    id: json['id'] as int,
    username: json['username'] as String,
    avatarUrl: json['avatar_url'] as String?,
    followersCount: (json['followers_count'] as int?) ?? 0,
    isVerified: (json['is_verified'] as bool?) ?? false,
  );
}

json_serializable 包

对于有许多模型的项目,json_serializable 包通过代码生成消除了重复的 fromJson/toJson 样板代码。设置包括三个步骤:

步骤 1:pubspec.yaml 设置

dependencies:
  flutter:
    sdk: flutter
  json_annotation: ^4.8.1

dev_dependencies:
  build_runner: ^2.4.0
  json_serializable: ^6.7.0

步骤 2:为模型添加注解

import 'package:json_annotation/json_annotation.dart';

part 'product.g.dart';

@JsonSerializable()
class Product {
  final int id;
  final String name;
  @JsonKey(name: 'unit_price')
  final double unitPrice;
  @JsonKey(defaultValue: true)
  final bool inStock;

  const Product({
    required this.id,
    required this.name,
    required this.unitPrice,
    required this.inStock,
  });

  factory Product.fromJson(Map<String, dynamic> json) =>
      _$ProductFromJson(json);

  Map<String, dynamic> toJson() => _$ProductToJson(this);
}

步骤 3:运行 Build Runner

# 一次性生成
flutter pub run build_runner build

# 监视模式——文件保存时自动重新生成
flutter pub run build_runner watch --delete-conflicting-outputs

运行 build_runner 后,会创建包含完整 fromJson 和 toJson 实现的 product.g.dart 文件。永远不要编辑此文件——始终编辑带注解的类并重新生成。

常用 @JsonKey 选项:

  • @JsonKey(name: 'snake_key')——将 JSON 键映射到名称不同的 Dart 字段。
  • @JsonKey(defaultValue: 0)——键缺失时提供默认值。
  • @JsonKey(ignore: true)——从序列化中排除字段。
  • @JsonKey(fromJson: _parseDate, toJson: _formatDate)——自定义转换函数。

freezed 包:不可变数据类

freezed 包构建在 json_serializable 之上,创建具有值相等性、copyWith() 和密封类支持的完全不可变 Dart 类。它是 Riverpod 和 BLoC 状态管理模型的首选:

dev_dependencies:
  build_runner: ^2.4.0
  json_serializable: ^6.7.0
  freezed: ^2.4.0

dependencies:
  freezed_annotation: ^2.4.0
  json_annotation: ^4.8.1
import 'package:freezed_annotation/freezed_annotation.dart';

part 'order.freezed.dart';
part 'order.g.dart';

@freezed
class Order with _$Order {
  const factory Order({
    required int id,
    required String status,
    required List<OrderItem> items,
    double? discount,
    @Default(false) bool isPriority,
  }) = _Order;

  factory Order.fromJson(Map<String, dynamic> json) =>
      _$OrderFromJson(json);
}

freezed 的优势:

  • 值相等性:具有相同字段的两个 Order 对象无需编写 hashCode 或 == 即可相等。
  • copyWith():轻松创建修改后的副本——order.copyWith(status: 'shipped')
  • @Default:内联声明默认字段值。
  • 密封类/联合类型:用模式匹配对 Loading | Data | Error 等状态建模。
@freezed
sealed class ApiState<T> with _$ApiState<T> {
  const factory ApiState.loading() = _Loading;
  const factory ApiState.data(T value) = _Data;
  const factory ApiState.error(String message) = _Error;
}

手动 fromJson/toJson 模式

有时你需要完全控制反序列化。以下是常见的手动模式:

DateTime 解析

factory Event.fromJson(Map<String, dynamic> json) {
  return Event(
    id: json['id'] as int,
    title: json['title'] as String,
    startTime: DateTime.parse(json['start_time'] as String),
    endTime: json['end_time'] != null
        ? DateTime.parse(json['end_time'] as String)
        : null,
    updatedAt: DateTime.fromMillisecondsSinceEpoch(
      (json['updated_at'] as int) * 1000,
    ),
  );
}

Map<String, dynamic> toJson() => {
  'id': id,
  'title': title,
  'start_time': startTime.toIso8601String(),
  'end_time': endTime?.toIso8601String(),
  'updated_at': updatedAt.millisecondsSinceEpoch ~/ 1000,
};

Map 字段

factory Config.fromJson(Map<String, dynamic> json) {
  return Config(
    settings: (json['settings'] as Map<String, dynamic>)
        .map((k, v) => MapEntry(k, v as String)),
    metadata: json['metadata'] as Map<String, dynamic>? ?? {},
  );
}

嵌套对象和列表

大多数真实世界的 JSON 都有嵌套对象和数组。以下是正确处理它们的方法:

class Author {
  final int id;
  final String name;

  Author({required this.id, required this.name});

  factory Author.fromJson(Map<String, dynamic> json) =>
      Author(id: json['id'] as int, name: json['name'] as String);

  Map<String, dynamic> toJson() => {'id': id, 'name': name};
}

class Post {
  final int id;
  final List<String> tags;
  final Author author;

  Post({required this.id, required this.tags, required this.author});

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'] as int,
      tags: List<String>.from(json['tags'] as List),
      author: Author.fromJson(json['author'] as Map<String, dynamic>),
    );
  }

  Map<String, dynamic> toJson() => {
    'id': id,
    'tags': tags,
    'author': author.toJson(),
  };
}

对于复杂对象列表:

factory Cart.fromJson(Map<String, dynamic> json) {
  return Cart(
    id: json['id'] as int,
    items: (json['items'] as List)
        .map((item) => CartItem.fromJson(item as Map<String, dynamic>))
        .toList(),
    discounts: json['discounts'] != null
        ? (json['discounts'] as List)
            .map((d) => Discount.fromJson(d as Map<String, dynamic>))
            .toList()
        : null,
  );
}

枚举序列化

将 Dart 枚举序列化为 JSON 字符串并反序列化需要谨慎——特别是当 API 可能返回尚未定义的值时:

enum OrderStatus {
  @JsonValue('pending') pending,
  @JsonValue('processing') processing,
  @JsonValue('shipped') shipped,
  @JsonValue('delivered') delivered,
  @JsonValue('cancelled') cancelled,
}

使用 json_serializable 时,用 @JsonKey(unknownEnumValue:) 优雅地处理未知值:

@JsonSerializable()
class Order {
  final int id;
  @JsonKey(unknownEnumValue: OrderStatus.pending)
  final OrderStatus status;

  Order({required this.id, required this.status});
  factory Order.fromJson(Map<String, dynamic> json) =>
      _$OrderFromJson(json);
  Map<String, dynamic> toJson() => _$OrderToJson(this);
}

对于手动序列化,扩展方法效果很好:

extension OrderStatusExtension on String {
  OrderStatus toOrderStatus() {
    switch (this) {
      case 'processing': return OrderStatus.processing;
      case 'shipped': return OrderStatus.shipped;
      case 'delivered': return OrderStatus.delivered;
      case 'cancelled': return OrderStatus.cancelled;
      default: return OrderStatus.pending;
    }
  }
}

status: (json['status'] as String).toOrderStatus(),

日期和时间处理

DateTime 处理是 JSON 序列化中最复杂的部分之一。以下是常见模式和可复用的自定义转换器:

class DateTimeConverter implements JsonConverter<DateTime, String> {
  const DateTimeConverter();

  @override
  DateTime fromJson(String json) => DateTime.parse(json);

  @override
  String toJson(DateTime object) => object.toIso8601String();
}

@JsonSerializable()
class Article {
  final int id;
  @DateTimeConverter()
  final DateTime publishedAt;

  Article({required this.id, required this.publishedAt});

  factory Article.fromJson(Map<String, dynamic> json) =>
      _$ArticleFromJson(json);
  Map<String, dynamic> toJson() => _$ArticleToJson(this);
}

使用 Dio 和 http 进行网络请求

将 JSON 解析与 HTTP 客户端结合是最常见的 Flutter 使用场景。以下是安全获取和解析 JSON 的方法:

使用 http 包

Future<User> fetchUser(int id) async {
  final response = await http.get(
    Uri.parse('https://api.example.com/users/$id'),
  );
  if (response.statusCode == 200) {
    return User.fromJson(
      jsonDecode(response.body) as Map<String, dynamic>,
    );
  }
  throw Exception('HTTP ${response.statusCode}');
}

使用 Dio

Future<User> fetchUser(int id) async {
  try {
    final response = await _dio.get<Map<String, dynamic>>('/users/$id');
    return User.fromJson(response.data!);
  } on DioException catch (e) {
    throw Exception('网络错误: ${e.message}');
  }
}

状态管理集成

生成的 Dart 模型与 Riverpod 和 BLoC 自然集成——这是 Flutter 最流行的两个状态管理解决方案。

带 AsyncNotifier 的 Riverpod

@riverpod
class UserNotifier extends _$UserNotifier {
  @override
  Future<User> build(int userId) => _fetchUser(userId);

  Future<User> _fetchUser(int id) async {
    final response = await http.get(
      Uri.parse('https://api.example.com/users/$id'),
    );
    return User.fromJson(
      jsonDecode(response.body) as Map<String, dynamic>,
    );
  }
}

BLoC 模式

class UserBloc extends Bloc<UserEvent, UserState> {
  UserBloc() : super(const UserState.initial()) {
    on<FetchUser>(_onFetchUser);
  }

  Future<void> _onFetchUser(
    FetchUser event, Emitter<UserState> emit,
  ) async {
    emit(const UserState.loading());
    try {
      final response = await http.get(
        Uri.parse('https://api.example.com/users/${event.id}'),
      );
      final user = User.fromJson(
        jsonDecode(response.body) as Map<String, dynamic>,
      );
      emit(UserState.loaded(user));
    } catch (e) {
      emit(UserState.error(e.toString()));
    }
  }
}

Hive 本地存储

Hive 是 Flutter 的快速轻量级本地数据库。你可以使用 HiveObject 和 TypeAdapter 持久化 JSON 派生的模型:

@HiveType(typeId: 0)
class CachedUser extends HiveObject {
  @HiveField(0) final int id;
  @HiveField(1) final String name;
  @HiveField(2) final String? email;

  CachedUser({required this.id, required this.name, this.email});

  factory CachedUser.fromUser(User user) =>
      CachedUser(id: user.id, name: user.name, email: user.email);
}
Future<void> cacheUser(User user) async {
  final box = Hive.box<CachedUser>('users');
  await box.put(user.id, CachedUser.fromUser(user));
}

CachedUser? getUser(int id) =>
    Hive.box<CachedUser>('users').get(id);

测试 JSON 模型

往返测试确保序列化和反序列化互为逆操作。为每个模型编写一个:

void main() {
  group('User model', () {
    final sampleJson = {
      'id': 1,
      'name': 'Alice',
      'email': 'alice@example.com',
      'created_at': '2024-01-15T10:30:00.000Z',
    };

    test('fromJson 创建正确的 User', () {
      final user = User.fromJson(sampleJson);
      expect(user.id, equals(1));
      expect(user.name, equals('Alice'));
    });

    test('往返保留所有字段', () {
      final user = User.fromJson(sampleJson);
      final restored = User.fromJson(user.toJson());
      expect(restored.id, equals(user.id));
      expect(restored.name, equals(user.name));
    });

    test('处理 null email', () {
      final jsonWithoutEmail = {...sampleJson}..remove('email');
      expect(User.fromJson(jsonWithoutEmail).email, isNull);
    });
  });
}

常见错误

以下是 Flutter 开发者在 JSON 序列化中最常见的错误:

1. 忘记运行 build_runner

添加或修改 @JsonSerializable() 类后,始终运行 `flutter pub run build_runner build --delete-conflicting-outputs`。.g.dart 文件不会自动更新。

2. 使用 dynamic 而非类型转换

// 不好——运行时静默传递错误类型
final name = json['name'];   // dynamic
// 好——用清晰的转换错误快速失败
final name = json['name'] as String;

3. 将缺失键视为 null

// 安全处理缺失键
final value = (json['maybe_missing'] ?? '') as String;

4. 列表类型擦除

// 不好——List<dynamic>,不是 List<String>
final tags = json['tags'] as List<String>; // 运行时 ClassCastException
// 好——显式转换
final tags = List<String>.from(json['tags'] as List);

5. 缺少 part 指令

json_serializable 和 freezed 都需要 part 指令。json_serializable:`part 'filename.g.dart';`。freezed 需要两个:`part 'filename.freezed.dart';` 和 `part 'filename.g.dart';`。

常见问题

json_serializable 和 freezed 有什么区别?

json_serializable 为可变类生成 fromJson 和 toJson 方法。freezed 构建在 json_serializable 之上,创建具有值相等性(== 和 hashCode)、copyWith() 和密封联合类型的不可变类。简单模型用 json_serializable,需要不可变性或状态管理联合类型时用 freezed。

每次修改模型都需要运行 build_runner 吗?

是的,但你可以使用监视模式避免手动运行:`flutter pub run build_runner watch --delete-conflicting-outputs`。这会监视文件并在保存时自动重新生成代码。

如何处理 JSON snake_case 键和 Dart camelCase 字段?

使用 json_serializable 时,在类上添加 @JsonSerializable(fieldRename: FieldRename.snake)。这会自动将 snake_case JSON 键映射到 camelCase Dart 字段名,无需在每个字段上单独使用 @JsonKey(name:) 注解。

如何解析顶层 JSON 数组?

当 API 在根级别返回 JSON 数组时,将其解码为 List 并映射每个元素:`final list = jsonDecode(response.body) as List; return list.map((e) => User.fromJson(e as Map<String, dynamic>)).toList();`

JSON 中缺少必填字段会发生什么?

如果用 `json['field'] as String` 转换,而键不存在,json['field'] 返回 null,转换会在运行时抛出类型转换错误。对可能缺失的字段始终使用空安全转换 (`as String?`),或用 `?? defaultValue` 提供默认值。

能在 Flutter Web 中使用 json_serializable 吗?

可以,json_serializable 与平台无关,在 Flutter Mobile、Web 和 Desktop 上的工作方式完全相同。生成的代码只使用 dart:core 类型。

如何序列化继承自另一个类的类?

在 @JsonSerializable 上使用 explicitToJson: true 选项并手动调用 super.toJson(),或者改用组合而非继承。json_serializable 不会自动包含父类字段,因此需要在 toJson() 中显式添加,或在父类上也使用 @JsonSerializable。

我应该为每个 API 响应生成 Dart 模型吗?

对于多次访问、在函数间传递或存储在状态中的任何 JSON 结构,都应生成模型。对于只读取一两个字段的一次性 API 调用,直接使用 jsonDecode 和 Map 访问是可以的。随着项目增长,类型化模型几乎总是值得的。

试用我们的免费 JSON 转 Dart 工具

JSON to Dart →
𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

DTJSON to Dart{ }JSON FormatterTSJSON to TypeScriptKTJSON to Kotlin

相关文章

JSON 转 TypeScript 在线指南:开发者完全手册

学习如何自动从 JSON 生成 TypeScript 类型。涵盖 interface 与 type、可选/可空字段、嵌套对象、联合类型、Zod 运行时验证、泛型 API 响应类型和 tsconfig 最佳实践。

JSON转Kotlin数据类:kotlinx.serialization、Moshi和Gson完整指南

在线将JSON转换为Kotlin数据类。学习使用kotlinx.serialization、Moshi和Gson进行JSON解析。