Code Generation with build_runner in Dart and Flutter
A practical guide to build_runner: builders, annotations, watch mode, build.yaml configuration, and how to keep generated code fast and out of your way.
Published on • October 8, 2026
AI Assistant

If you have ever written toJson, fromJson, copyWith, or an == overload by hand in Dart, you have felt the problem build_runner exists to solve. build_runner is the build system behind Dart’s code generation ecosystem — serialization, data classes, routing, dependency injection, mocking, theming, and more.
The package describes itself simply: “A build system for Dart code generation and modular compilation.” On pub.dev it ships as build_runner: ^2.16.2 from the official tools.dart.dev publisher, with roughly 6.7 million lifetime downloads.
What a builder actually is
A build_runner generator is called a builder. In the words of the official docs, “a builder adds some capability to your code that is inconvenient to add and maintain in pure Dart. Examples include serialization, data classes, data binding, dependency injection, and mocking.”
Builders come from separate packages. A representative selection from pub.dev:
| Builder | Adds capabilities | Notes |
|---|---|---|
json_serializable | JSON serialization | Flutter Favorite |
freezed | Data classes, tagged unions, cloning | Flutter Favorite |
built_value_generator | Data classes with JSON | Flutter Favorite |
riverpod_generator | Reactive caching and data binding | Flutter Favorite |
drift_dev | Reactive data binding and SQL | |
go_router_builder | Type-safe Flutter navigation | by Google |
injectable_generator | Dependency injection | |
mockito | Mocks and fakes for testing | by Google |
hive_ce_generator | Key-value database bindings | |
flutter_gen_runner | Flutter asset bindings | |
slang_build_runner | Type-safe i18n |
Notice that several are Flutter Favorites — code generation is not a niche concern, it is the mainstream way Flutter apps handle boilerplate.
Getting started
Most builders are activated by an annotation. A minimal json_serializable setup:
# pubspec.yaml
dependencies:
json_annotation: ^4.9.0
dev_dependencies:
build_runner: ^2.16.2
json_serializable: ^6.10.0
import 'package:json_annotation/json_annotation.dart';
part 'person.g.dart';
@JsonSerializable()
class Person {
final String name;
final DateTime? dateOfBirth;
Person({required this.name, this.dateOfBirth});
Map<String, dynamic> toJson() => _$PersonToJson(this);
factory Person.fromJson(Map<String, dynamic> json) =>
_$PersonFromJson(json);
}
Then run a build:
dart run build_runner build
Or switch to watch mode, which regenerates whenever sources change:
dart run build_runner watch
Watch mode is where build_runner earns its keep. Add a field to Person, save the file, and the generated toJson/fromJson update before you finish typing.
The commands you will actually use
# One-shot build
dart run build_runner build
# Regenerate everything from scratch (fixes most "stale output" issues)
dart run build_runner build --delete-conflicting-outputs
# Watch mode during development
dart run build_runner watch
# Serve generated output over HTTP (webdev)
dart run build_runner serve
--delete-conflicting-outputs is the single most useful flag. When a builder’s output already exists but does not match what it would generate now — after a package upgrade, say — the build fails rather than overwriting. Deleting and regenerating resolves it.
Workspaces. If you use Dart pub workspaces, you can build several packages at once with the --workspace flag. This is still experimental, but it is the right answer for monorepos where a model package and an app package need generation together.
Output files and source control
Outputs written under lib are immediately available to compilers and IDEs. The docs are explicit about the trade-off:
You can choose whether or not to check generated files into source control. If you publish your package, you must publish the generated files with it. Users getting your package via
pubcannot run the build step themselves.
Practical guidance:
- Applications: checking generated files in is fine and usually preferable — CI becomes faster and diffs show what generation changed.
- Packages: you must ship the generated files.
- Either way:
.dart_tool/is build_runner’s private scratch space and must be gitignored.
# .gitignore
.dart_tool/
The docs warn that .dart_tool/build artifacts and .dart_tool/build/generated outputs “are internal to the build… private to build_runner and should not be edited, checked in, published or used in any other way.”
Configuring builders with build.yaml
When you need to control where, when, or how a builder runs, add a build.yaml at your package root.
Restrict a builder to specific files:
targets:
$default:
builders:
json_serializable:
generate_for:
- lib/models/*.dart
Pass options to a builder:
targets:
$default:
builders:
freezed:
options:
format: true
copy_with: false
equal: false
Control build order when two builders depend on each other’s output — the build_config package documents the full build_overrides and ordering model. Most of the time you will not need it; when you do, it is usually because a generated part file must exist before another builder parses the library.
Performance: keeping builds fast
build_runner runs the build in a child process, which has two consequences worth knowing.
1. Incremental builds are the norm, full builds are not. Watch mode only rebuilds what changed. If a build feels slow, check whether something is invalidating the whole graph — editing build.yaml, upgrading a builder package, or touching a shared part file will do it.
2. Debugging requires passing VM args through.
dart run build_runner build \
--dart-jit-vm-arg=--observe \
--dart-jit-vm-arg=--pause-isolates-on-start
That prints a DevTools URL you can attach to with “Debug: Attach to Dart Process” in VS Code.
Other practical speed-ups:
- Use
generate_forto stop builders scanning folders they will never emit into. - Keep generated files out of analysis hot paths where possible.
- Split large models across files so only affected libraries rebuild.
- Run
dart run build_runner cleanwhen outputs get into an inconsistent state.
Writing your own builder
For advanced cases you can write a builder against the build package. A builder is a function from a BuildStep to one or more outputs — read input assets, run the analyzer, emit a .g.dart file. Test it with build_test.
You rarely need this. The builders above cover serialization, immutability, routing, DI, i18n, assets, theming, and database bindings. Reaching for a custom builder usually means a codebase-specific pattern worth extracting into a shared package instead.
Common failure modes
“Conflicting outputs.” Another build owns the file. Run with --delete-conflicting-outputs.
Generated file not found after pulling changes. Someone bumped a builder. Run a fresh build; IDE restarts rarely fix it.
part directive missing. Builders emit into part 'x.g.dart' — the annotation and the part line must both exist or the build silently skips the library.
Builds in CI are slow. Cache .dart_tool between runs, and prefer build over watch with a deterministic --delete-conflicting-outputs.
Analyzer errors inside .g.dart. Never edit generated files; your changes vanish on the next build. Fix the annotated source instead.
Wrapping up
build_runner is the connective tissue of the Dart ecosystem: annotations declare intent, builders emit the mechanical code, and watch mode keeps everything in sync while you work. Learn the two commands (build and watch), the one flag (--delete-conflicting-outputs), and the one file (build.yaml), and the entire code generation workflow stops being mysterious.
References
- build_runner package — builders list, getting started, configuration, and debugging
- build package — writing your own builder
- build_config package — build.yaml configuration reference
- Dart pub workspaces