Dart: Dart 代码生成 — build_runner 与序列化自动化

代码生成是消灭样板代码的终极武器 — 让机器写代码,让人写逻辑。

1. 你将学到


2. 一个开发者的真实故事

(1) 痛点:20 个模型类手动序列化花了 3 天

Bob 的 DataPipeline 有 20 个数据模型类,每个都需要 fromJson/toJson 方法。手动编写 20 个类的序列化代码花了 3 天,期间出了 4 次拼写错误和 2 次类型转换错误。更糟的是,每次新增字段都要手动更新 3 处代码(字段声明、fromJson、toJson),一次漏改导致 100,000 条记录解析失败。

(2) json_serializable 的解法

用 @JsonSerializable 注解标注模型类,build_runner 自动生成 fromJson/toJson。新增字段只需声明 + 重新 build,不会再遗漏。

DART
import 'package:json_annotation/json_annotation.dart';
part 'order.g.dart';

@JsonSerializable()
class Order {
  final String id;
  final double amount;
  Order({required this.id, required this.amount});

  factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
  Map<String, dynamic> toJson() => _$OrderToJson(this);
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

(3) 收益


3. build_runner 工作原理

(1) 生成流水线

100%
flowchart LR
  A["model.dart<br/>@JsonSerializable"] --> B[build_runner]
  B --> C["model.g.dart<br/>fromJson/toJson"]
  B --> D["model.freezed.dart<br/>copyWith/equals"]
  A --> E[".part directive"]
  E --> C
  subgraph Generation Pipeline
    B
    C
    D
  end
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
组件 职责
Builder 读取源文件,决定生成什么

4. json_serializable

(1) 配置与使用

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:pubspec.yaml 配置

YAML
dependencies:
  json_annotation: ^4.8.0

dev_dependencies:
  build_runner: ^2.4.0
  json_serializable: ^6.7.0

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:基本模型类

DART
// lib/src/models/order.dart
import 'package:json_annotation/json_annotation.dart';

part 'order.g.dart';

@JsonSerializable()
class Order {
  final String id;
  final double amount;
  final String status;
  final String? category;

  Order({
    required this.id,
    required this.amount,
    required this.status,
    this.category,
  });

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

// Run: dart run build_runner build
// Generates order.g.dart with _$OrderFromJson and _$OrderToJson
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:自定义字段映射

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

part 'product.g.dart';

@JsonSerializable()
class Product {
  @JsonKey(name: 'product_id')
  final String id;

  @JsonKey(name: 'product_name')
  final String name;

  @JsonKey(name: 'unit_price')
  final double price;

  @JsonKey(defaultValue: 'General')
  final String category;

  @JsonKey(fromJson: _dateTimeFromEpoch, toJson: _dateTimeToEpoch)
  final DateTime createdAt;

  @JsonKey(ignore: true)
  final String? cachedData;

  Product({
    required this.id,
    required this.name,
    required this.price,
    this.category = 'General',
    required this.createdAt,
    this.cachedData,
  });

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

DateTime _dateTimeFromEpoch(int epoch) => DateTime.fromMillisecondsSinceEpoch(epoch);
int _dateTimeToEpoch(DateTime dt) => dt.millisecondsSinceEpoch;
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:嵌套对象序列化

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

part 'customer.g.dart';

@JsonSerializable()
class Address {
  final String city;
  final String? state;
  final String country;

  Address({required this.city, this.state, required this.country});

  factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
  Map<String, dynamic> toJson() => _$AddressToJson(this);
}

@JsonSerializable()
class Customer {
  final String name;
  final String email;
  final Address? address;

  Customer({required this.name, required this.email, this.address});

  factory Customer.fromJson(Map<String, dynamic> json) => _$CustomerFromJson(json);
  Map<String, dynamic> toJson() => _$CustomerToJson(this);
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
@JsonKey 参数 含义 示例
name JSON 中的键名 @JsonKey(name: 'product_id')
defaultValue 缺失时的默认值 @JsonKey(defaultValue: 'N/A')
fromJson 自定义反序列化函数 @JsonKey(fromJson: _parse)
toJson 自定义序列化函数 @JsonKey(toJson: _format)
ignore 忽略该字段 @JsonKey(ignore: true)

5. freezed

(1) 不可变数据类生成

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:freezed 基本用法

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

part 'order_event.freezed.dart';
part 'order_event.g.dart';

@freezed
class OrderEvent with _$OrderEvent {
  const factory OrderEvent.created({
    required String orderId,
    required double amount,
    required DateTime timestamp,
  }) = OrderCreated;

  const factory OrderEvent.statusChanged({
    required String orderId,
    required String from,
    required String to,
  }) = OrderStatusChanged;

  const factory OrderEvent.cancelled({
    required String orderId,
    required String reason,
  }) = OrderCancelled;

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

// Run: dart run build_runner build
// Generates:
// - order_event.freezed.dart: copyWith, ==, hashCode, toString, pattern matching
// - order_event.g.dart: fromJson, toJson
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
freezed 生成 功能
copyWith() 不可变副本
== / hashCode 值相等
toString() 格式化输出
when() 模式匹配回调
maybeWhen() 可选模式匹配
fromJson/toJson JSON 序列化

6. build_runner 命令

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:常用命令

BASH
# One-time build
dart run build_runner build

# Watch mode - rebuild on file changes
dart run build_runner watch

# Clean generated files
dart run build_runner clean

# Build with deletion of stale files
dart run build_runner build --delete-conflicting-outputs

# Watch with deletion
dart run build_runner watch --delete-conflicting-outputs
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
命令 用途 开发阶段
build 一次性生成 发布
watch 监听变化自动生成 开发
clean 清除生成文件 重置
--delete-conflicting-outputs 自动覆盖冲突 调试

7. Part 文件机制

(1) part 与 part of

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:Part 文件关系

DART
// lib/models/order.dart (source file)
import 'package:json_annotation/json_annotation.dart';

// Declare part files
part 'order.g.dart';        // Generated by json_serializable
part 'order.freezed.dart';  // Generated by freezed (if used)

@JsonSerializable()
class Order {
  final String id;
  final double amount;
  Order({required this.id, required this.amount});

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

// Generated: order.g.dart
// part of 'order.dart';
// Order _$OrderFromJson(Map<String, dynamic> json) => Order(...)
// Map<String, dynamic> _$OrderToJson(Order instance) => {...}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。
指令 位置 含义
part 'file.dart' 源文件 声明 part 文件
part of 'file.dart' 生成文件 属于哪个源文件
.g.dart 生成文件 json_serializable
.freezed.dart 生成文件 freezed

8. 自定义 Builder 入门

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:简单 Builder 概念

DART
// Custom builder concept (simplified)
// In a real project, this would be in a separate package

// A custom annotation
class CsvMapping {
  final String columnName;
  final bool required;

  const CsvMapping({required this.columnName, this.required = true});
}

// Model using the annotation
class Order {
  @CsvMapping(columnName: 'order_id')
  final String id;

  @CsvMapping(columnName: 'total_amount', required: false)
  final double amount;

  Order({required this.id, required this.amount});
}

// What a custom builder would generate:
// order.mapper.dart
// part of 'order.dart';
//
// Order OrderFromCsv(Map<String, String> row) => Order(
//   id: row['order_id'] ?? '',
//   amount: double.tryParse(row['total_amount'] ?? '0') ?? 0,
// );
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

9. Bob 场景:DataPipeline 序列化代码生成

▶ 示例

TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

:完整模型定义

DART
// lib/src/models/order.dart
import 'package:json_annotation/json_annotation.dart';

part 'order.g.dart';

@JsonSerializable(createToJson: true, createFactory: true)
class Order {
  @JsonKey(name: 'order_id')
  final String id;

  @JsonKey(name: 'total_amount')
  final double amount;

  @JsonKey(name: 'order_status', defaultValue: 'pending')
  final String status;

  @JsonKey(name: 'product_category', required: false)
  final String? category;

  @JsonKey(name: 'discount_rate', defaultValue: 0)
  final double discountRate;

  @JsonKey(name: 'created_at')
  final DateTime createdAt;

  Order({
    required this.id,
    required this.amount,
    this.status = 'pending',
    this.category,
    this.discountRate = 0,
    DateTime? createdAt,
  }) : createdAt = createdAt ?? DateTime.now();

  // Generated by build_runner
  factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
  Map<String, dynamic> toJson() => _$OrderToJson(this);

  // Computed properties (not in JSON)
  double get tax => amount * 0.08;
  double get total => amount * (1 - 0.08) * (1 - discountRate);
  String get formatAmount => '\$${amount.toStringAsFixed(2)} USD';
}

// After running: dart run build_runner build
// The order.g.dart file is generated with:
// - _$OrderFromJson: parses JSON to Order
// - _$OrderToJson: serializes Order to JSON
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

10. 完整示例:DataPipeline 模型序列化

DART
// ============================================
// DataPipeline Model Serialization
// Complete models with json_serializable
// ============================================

import 'package:json_annotation/json_annotation.dart';

part 'models.g.dart';

// Order model
@JsonSerializable()
class Order {
  @JsonKey(name: 'order_id')
  final String id;

  @JsonKey(name: 'total_amount')
  final double amount;

  @JsonKey(defaultValue: 'pending')
  final String status;

  @JsonKey(name: 'category')
  final String? category;

  @JsonKey(name: 'discount_rate', defaultValue: 0.0)
  final double discountRate;

  @JsonKey(name: 'region', defaultValue: 'US')
  final String region;

  Order({
    required this.id,
    required this.amount,
    this.status = 'pending',
    this.category,
    this.discountRate = 0.0,
    this.region = 'US',
  });

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

  double get effectiveAmount => amount * (1 - discountRate);
  double get tax => effectiveAmount * 0.08;
  double get total => effectiveAmount + tax;
  String get formatTotal => '\$${total.toStringAsFixed(2)} USD';
}

// Report model
@JsonSerializable()
class Report {
  final String title;
  final int totalOrders;
  final int completedOrders;
  final double totalRevenue;
  final double totalTax;
  final Map<String, double> revenueByCategory;
  final DateTime generatedAt;

  Report({
    required this.title,
    required this.totalOrders,
    required this.completedOrders,
    required this.totalRevenue,
    required this.totalTax,
    required this.revenueByCategory,
    DateTime? generatedAt,
  }) : generatedAt = generatedAt ?? DateTime.now();

  factory Report.fromJson(Map<String, dynamic> json) => _$ReportFromJson(json);
  Map<String, dynamic> toJson() => _$ReportToJson(this);

  String get formatRevenue => '\$${totalRevenue.toStringAsFixed(2)} USD';
  double get averageOrderValue => completedOrders > 0 ? totalRevenue / completedOrders : 0;
}

// Simulated usage (in production, .g.dart would be generated)
void main() {
  // Simulating what the generated code would do
  final json = {
    'order_id': 'ORD-001',
    'total_amount': 1500.0,
    'status': 'completed',
    'category': 'Electronics',
    'discount_rate': 0.1,
    'region': 'US',
  };

  // Manual parsing (simulating _$OrderFromJson)
  final order = Order(
    id: json['order_id'] as String,
    amount: (json['total_amount'] as num).toDouble(),
    status: json['status'] as String? ?? 'pending',
    category: json['category'] as String?,
    discountRate: (json['discount_rate'] as num?)?.toDouble() ?? 0.0,
    region: json['region'] as String? ?? 'US',
  );

  print('=== DataPipeline Order ===');
  print('ID:       ${order.id}');
  print('Amount:   \$${order.amount.toStringAsFixed(2)} USD');
  print('Discount: ${(order.discountRate * 100).toStringAsFixed(0)}%');
  print('Tax:      \$${order.tax.toStringAsFixed(2)} USD');
  print('Total:    ${order.formatTotal}');
  print('Category: ${order.category ?? "N/A"}');
  print('Region:   ${order.region}');

  // Simulate report generation
  final report = Report(
    title: 'Daily Analytics Report',
    totalOrders: 1000,
    completedOrders: 850,
    totalRevenue: 525000.0,
    totalTax: 42000.0,
    revenueByCategory: {
      'Electronics': 360000.0,
      'Clothing': 89000.0,
      'Books': 76000.0,
    },
  );

  print('\n=== Report ===');
  print('Title:    ${report.title}');
  print('Orders:   ${report.completedOrders}/${report.totalOrders}');
  print('Revenue:  ${report.formatRevenue}');
  print('Average:  \$${report.averageOrderValue.toStringAsFixed(2)} USD');
}
TEXT 📖 仅展示
> **输出:** 在本地 DartPad 或 `dart run` 执行。Dart 课程所有示例基于 Dart 3.x / Flutter 3.x,运行结果会因 SDK 版本略有差异。

输出:

TEXT 📖 仅展示
=== DataPipeline Order ===
ID:       ORD-001
Amount:   $1500.00 USD
Discount: 10%
Tax:      $108.00 USD
Total:    $1458.00 USD
Category: Electronics
Region:   US

=== Report ===
Title:    Daily Analytics Report
Orders:   850/1000
Revenue:  $525000.00 USD
Average:  $617.65 USD

❓ 常见问题

Q:build_runner watch 模式会很慢吗? A:首次构建需要扫描所有文件,较慢(10-30 秒)。后续增量构建只处理修改的文件,通常 1-3 秒。大项目可用 --build-filter 只生成指定文件。

Q:json_serializable 和手写 fromJson 性能有差异吗? A:几乎没有。生成的代码与手写代码质量相当,甚至在某些场景下更优(使用更精确的类型检查)。

Q:freezed 和 json_serializable 必须一起用吗? A:不是。json_serializable 负责序列化,freezed 负责不可变类生成。可以只用 json_serializable,也可以两者组合使用。

Q:生成文件需要提交到版本控制吗? A:应用项目建议提交(确保 CI 不需要 build_runner);库项目建议提交(pub.dev 发布需要)。也有一些团队选择 .gitignore 生成文件。

Q:如何调试生成的代码? A:直接打开 .g.dart 文件阅读。生成的代码是标准 Dart 代码,可以设断点、加 print。问题通常在注解配置上,不在生成逻辑。

Q:build_runner 和 source_gen 有什么关系? A:source_gen 是 build_runner 的上层抽象,提供更简单的 API 来编写自定义 Builder。json_serializable 和 freezed 都基于 source_gen。

Q:可以自定义 @JsonKey 的 fromJson/toJson 类型吗? A:可以。定义顶层函数或静态方法,签名需匹配 T fromJson(Object? json)Object toJson(T value)。在 @JsonKey 中引用即可。


📖 小节


📝 作业

  1. 基础题(难度⭐):创建一个 Dart 项目,添加 json_serializable 和 build_runner 依赖。为一个简单的 Product 类(3 个字段)添加 @JsonSerializable 注解,运行 dart run build_runner build,查看生成的 .g.dart 文件。
  2. 进阶题(难度⭐⭐):为一个嵌套模型(Order 包含 List<Product>,Product 包含 Category 枚举)配置 json_serializable,处理自定义字段名和默认值。验证 fromJson/toJson 的往返正确性。
  3. 挑战题(难度⭐⭐⭐):结合 freezed 和 json_serializable,为 DataPipeline 创建一个 Sealed Class 风格的 OrderEvent 模型(Created/StatusChanged/Cancelled),生成不可变类 + JSON 序列化 + copyWith。编写测试验证生成代码。

← 上一课 | 下一课 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏