Dart: Dart Code Generation — build_runner and Serialization

Last updated: 2026-08-26

Code generation is the ultimate weapon to eliminate boilerplate — let machines write code, and let humans write logic.

1. What You Will Learn


2. A Real Developer's Story

(1) The Pain Point: 3 Days Spent Manually Serializing 20 Model Classes

Bob's DataPipeline has 20 data model classes, each requiring fromJson/toJson methods. Manually writing serialization code for 20 classes took 3 days, during which 4 typos and 2 type conversion errors occurred. Worse, every time a new field was added, 3 places in the code had to be updated manually (field declaration, fromJson, toJson). One missed update caused the parsing of 100,000 records to fail.

(2) json_serializable's Solution

Annotate the model class with @JsonSerializable, and build_runner automatically generates fromJson/toJson. Adding a new field only requires declaration + re-running the build, with no risk of omission.

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 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.

(3) The Benefits


3. How build_runner Works

(1) Generation Pipeline

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 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.
Component Responsibility
Builder Reads source files, determines what to generate
Generator Contains the specific code generation logic
AssetReader Reads source code files
AssetWriter Writes the generated files
Part files .g.dart / .freezed.dart

4. json_serializable

(1) Configuration and Usage

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.

: pubspec.yaml configuration

YAML
dependencies:
  json_annotation: ^4.8.0

dev_dependencies:
  build_runner: ^2.4.0
  json_serializable: ^6.7.0

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.

: Basic model class

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 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.

: Custom field mapping

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 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.

: Nested object serialization

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 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.
@JsonKey Parameter Meaning Example
name Key name in JSON @JsonKey(name: 'product_id')
defaultValue Default value when missing @JsonKey(defaultValue: 'N/A')
fromJson Custom deserialization function @JsonKey(fromJson: _parse)
toJson Custom serialization function @JsonKey(toJson: _format)
ignore Ignore this field @JsonKey(ignore: true)

5. freezed

(1) Immutable Data Class Generation

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.

: Basic freezed usage

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 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.
freezed Generates Feature
copyWith() Immutable copy
== / hashCode Value equality
toString() Formatted output
when() Pattern matching callback
maybeWhen() Optional pattern matching
fromJson/toJson JSON serialization

6. build_runner Commands

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.

: Common commands

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 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.
Command Purpose Development Stage
build One-time generation Release
watch Auto-generate on changes Development
clean Remove generated files Reset
--delete-conflicting-outputs Auto-overwrite conflicts Debugging

7. Part File Mechanism

(1) part and part of

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.

: Part file relationship

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 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.
Directive Location Meaning
part 'file.dart' Source file Declares a part file
part of 'file.dart' Generated file Indicates which source file it belongs to
.g.dart Generated file From json_serializable
.freezed.dart Generated file From freezed

8. Introduction to Custom Builders

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.

: Simple Builder concept

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 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.

9. Bob's Scenario: DataPipeline Serialization Code Generation

▶ Example

TEXT 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.

: Complete model definition

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 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.

10. Complete Example: DataPipeline Model Serialization

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 📖 Display only
> **Output:** Run in local DartPad or with `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x. Results may vary slightly depending on the SDK version.

Output:

TEXT 📖 Display only
=== 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

❓ FAQ

Q: Will build_runner watch mode be slow? A: The initial build needs to scan all files and can be slow (10-30 seconds). Subsequent incremental builds only process modified files, usually taking 1-3 seconds. For large projects, use --build-filter to generate only specified files.

Q: Is there a performance difference between json_serializable and handwritten fromJson? A: Practically none. The generated code has similar quality to handwritten code, and in some scenarios it may even be better (using more precise type checks).

Q: Must freezed and json_serializable be used together? A: No. json_serializable handles serialization, while freezed handles immutable class generation. You can use json_serializable alone, or combine both.

Q: Should generated files be committed to version control? A: For application projects, it is recommended to commit (to ensure CI doesn't require build_runner); for library projects, it is recommended to commit (since pub.dev publication requires them). Some teams choose to .gitignore generated files.

Q: How to debug generated code? A: Open the .g.dart file directly to read it. The generated code is standard Dart code; you can set breakpoints, add print statements. Issues are usually in the annotation configuration, not in the generation logic.

Q: What is the relationship between build_runner and source_gen? A: source_gen is a higher-level abstraction for build_runner, providing a simpler API for writing custom Builders. Both json_serializable and freezed are built on source_gen.

Q: Can you customize the types for @JsonKey's fromJson/toJson? A: Yes. Define a top-level function or static method. The signature should match T fromJson(Object? json) and Object toJson(T value). Then reference it in @JsonKey.


📖 Summary


📝 Exercises

  1. Basic (Difficulty ⭐): Create a Dart project, add json_serializable and build_runner dependencies. Add the @JsonSerializable annotation to a simple Product class (with 3 fields), run dart run build_runner build, and examine the generated .g.dart file.
  2. Intermediate (Difficulty ⭐⭐): Configure json_serializable for a nested model (an Order containing a List<Product>, where Product contains a Category enum). Handle custom field names and default values. Verify the round-trip correctness of fromJson/toJson.
  3. Challenge (Difficulty ⭐⭐⭐): Combine freezed and json_serializable to create a Sealed Class style OrderEvent model for DataPipeline (Created/StatusChanged/Cancelled). Generate immutable classes + JSON serialization + copyWith. Write tests to verify the generated code.

← Previous lesson | Next lesson →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

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

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