skip to content

Build Runner Generators

build_runner runs builders such as source_gen generators over annotated code and writes .g.dart part files, once or in watch mode. Interviewers probe that loop and why macros were cancelled.

part ofDartoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In a Dart or Flutter project, what does dart run build_runner build do, and when do you use watch or clean instead?

level: juniorimportance: must knowfreq 55%

answer

  1. builders are dev dependencies
  2. one pass versus a file watcher
  3. part directive must already exist
  4. cache lives in .dart_tool/build
  5. -d has been ignored since 2.7

basics

~20 s

build_runner build runs every applicable builder once and writes outputs such as .g.dart parts beside your sources; watch keeps running and rebuilds incrementally on each save; clean deletes the build cache so the next build is a full one.

solid answer

~40 s

`build_runner` drives **builders**: code generators such as `json_serializable`, `freezed` or `mockito`, added as dev dependencies. `dart run build_runner build` runs them once over the package and writes outputs like `person.g.dart`, which the source pulls in with `part 'person.g.dart';`. `dart run build_runner watch` stays running and rebuilds only what changed each time you save, so generated `fromJson` or `copyWith` code tracks your edits. `dart run build_runner clean` deletes the package's build cache under `.dart_tool/build`; the next build then starts from scratch, which helps when a build behaves oddly. `stop` ends a running `watch`. The old `-d`/`--delete-conflicting-outputs` flag has been ignored since build_runner 2.7: conflicting generated files are always deleted, and since 2.16 a hand-edited generated file is overwritten on the next build.

code

bash · 5 lines
bash
dart pub get
dart run build_runner build   # one pass, writes *.g.dart
dart run build_runner watch   # rebuild on every save
dart run build_runner stop    # end a running watch
dart run build_runner clean   # drop .dart_tool/build cache

go deeper

for a junior

Recall the three commands, the part directive the source must declare, and that annotations go in dependencies while generators go in dev_dependencies.

for a middle

Explain incremental rebuilds in watch, where the cache lives, and why -d stopped mattering in build_runner 2.7.

for a senior

Show you can unstick a broken build: clean, check part names and generator versions, and know the 2.16 overwrite behaviour.

for a principal

Weigh how much codegen a project should depend on, given every developer and CI job must run build_runner before the code compiles.

## What build_runner is Dart has no run-time reflection in Flutter apps (`dart:mirrors` is disabled there) and no macros, so boilerplate such as JSON mapping, `copyWith`, value equality or test mocks is **generated ahead of time**. `build_runner` is the command-line driver for that generation. The generators themselves are called **builders** and ship in ordinary pub packages: - `json_serializable`: `fromJson`/`toJson` for annotated classes; - `freezed`: immutable data classes, `copyWith`, equality; - `mockito`: mock classes for tests; - `riverpod_generator`, `go_router_builder`, `drift_dev` and many others. A typical setup puts the **annotation** package in `dependencies` (it is used by your code at run time) and `build_runner` plus the **generator** in `dev_dependencies` (they only run on your machine or CI): ```yaml dependencies: json_annotation: ^4.9.0 dev_dependencies: build_runner: ^2.16.0 json_serializable: ^6.10.0 ``` Your source declares the part it expects, and references generated members: ```dart import 'package:json_annotation/json_annotation.dart'; part 'person.g.dart'; @JsonSerializable() class Person { Person({required this.name}); final String name; factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json); Map<String, dynamic> toJson() => _$PersonToJson(this); } ``` Until the first build runs, `_$PersonFromJson` does not exist and the analyzer shows errors. That is normal. ## The commands | Command | What it does | Typical use | |---|---|---| | `dart run build_runner build` | one full or incremental build, then exit | after pulling changes, in CI | | `dart run build_runner watch` | builds, then rebuilds on every file change | while editing models | | `dart run build_runner clean` | deletes the package or workspace build cache | when a build is confused | | `dart run build_runner stop` | ends a running `watch` or `serve` | freeing the lock | | `dart run build_runner serve` / `test` | build plus a dev server or test run | web and test workflows | `watch` is **incremental**: it tracks which inputs each output was built from and reruns only the builders whose inputs changed. Since build_runner 2.9 it also picks up `build.yaml` changes without restarting. ## Where files go - **Package path**: outputs such as `lib/models/person.g.dart` land next to their inputs, visible to the analyzer, compilers and your IDE. - **Artifact tree**: internal and hidden outputs live under `.dart_tool/build/generated`, and the rest of `.dart_tool/build` holds the build's cache and graph. These are private to `build_runner`; git should ignore `.dart_tool`. If a source file annotates a class but lacks the matching `part` directive, source_gen logs a warning telling you which `part` line to add, and no generated file is written for that library. ## Flags you will meet in old tutorials 1. **`-d` / `--delete-conflicting-outputs`**: before build_runner 2.7, if a generated file already existed that the build had not produced itself, the tool prompted or refused, and `-d` told it to delete such files. Since 2.7.0 the flag is **ignored**: build_runner always deletes conflicting outputs as if `-d` were passed, and the interactive prompt is gone. Scripts that still pass it keep working. 2. **`flutter pub run build_runner`**: older spelling; `dart run build_runner` is the current one in both Dart and Flutter projects. 3. **`--low-resources-mode`**, **`--track-performance`**: removed in 2.15; still accepted with a warning and ignored. ## Behaviour worth knowing - Since build_runner **2.16**, a build **fixes incorrect generated files**: if you hand-edit `person.g.dart` and build again, your edit is overwritten. `--keep-modified-outputs` restores the old behaviour. - Since **2.14**, builders are **AOT-compiled** by default for commands other than `run`, which costs startup time on the first build but speeds up builds afterwards; builders using `dart:mirrors` fall back to JIT. - Only one `build_runner` command runs per package at a time; a new one waits for, or stops, a running `watch`. ## A day with build_runner 1. **After cloning or pulling**: `dart pub get`, then `dart run build_runner build`, so every `.g.dart` and `.freezed.dart` matches the current models. 2. **While editing models**: leave `dart run build_runner watch` running in a terminal. Each save triggers an incremental rebuild of only the affected outputs, usually in a second or two once the builders are compiled. 3. **When switching branches**: `watch` notices the changed files and rebuilds; if results look wrong afterwards, stop it, run `clean`, and build again. 4. **Before pushing**: make sure the generated files match the sources, and let CI verify it. ## What interviewers listen for - That you know generated code is **produced by a separate step**, not by the compiler, so it can be missing or stale. - That you can name the generated file, the `part` line and the generated private helper (`_$PersonFromJson`) and explain how they connect. - That you do not reach for `-d` or `clean` as rituals: `-d` does nothing today, and `clean` is for a confused cache, not for every build.

  • Why do tutorials tell you to add -d, and do you still need it?
    Older build_runner versions refused or prompted when a generated file already on disk had not been produced by the current build, and `--delete-conflicting-outputs` (`-d`) told it to delete such files. Since build_runner 2.7 the flag is ignored and conflicting outputs are always deleted, so it is harmless but unnecessary.
  • The IDE shows _$PersonFromJson as undefined right after cloning a repo; what is wrong?
    Usually nothing but a missing build: the generated part has not been created yet, either because the team does not commit `.g.dart` files or because the model changed. Run `dart run build_runner build`. If it still fails, check that the file declares `part 'person.g.dart';` with the right name.
  • When is clean the right tool rather than another build?
    When the incremental state itself seems wrong: outputs that do not update, errors that persist after the cause is fixed, or after switching between a workspace build and a single-package build. `clean` deletes the cache under `.dart_tool/build`, so the next build redoes everything from sources.

saying these in an interview costs you the question

  • Adds build_runner and json_serializable under dependencies instead of dev_dependencies
  • Believes -d is still required to overwrite existing generated files
  • Expects generated code to exist without ever running build_runner
  • Edits a .g.dart file by hand and expects the change to survive
  • Runs clean before every build as a ritual, making each build a full one
open as a page

In a Dart package's build.yaml, how do targets, generate_for and builder options configure the generators build_runner runs?

level: middleimportance: should knowfreq 30%

basics

~20 s

build.yaml's targets: $default: builders: section configures each builder applied to the package: generate_for limits it to matching files, enabled switches it on or off, and options passes settings such as json_serializable's explicit_to_json to every generated class.

open as a page

Why did the Dart team cancel macros, and what does that mean for code generation with build_runner in a Dart 3.13 project?

level: middleimportance: should knowfreq 30%

basics

~20 s

Macros needed deep semantic introspection at compile time, which slowed static analysis, code completion and the incremental compile behind hot reload, so the Dart team stopped work in January 2025. build_runner generators remain the way to generate code.

open as a page

A Flutter app's generated fromJson, toJson and copyWith code keeps going stale in CI; do you commit the build_runner output, and how does --only-check help?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Either commit .g.dart and .freezed.dart files and have CI run dart run build_runner build --only-check, which writes nothing and fails on any difference, or commit nothing and generate in CI before analyzing. Published packages must ship generated files.

open as a page

In Dart's source_gen, how does SharedPartBuilder let several generators share one .g.dart file, and how do PartBuilder and LibraryBuilder differ?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

Each SharedPartBuilder writes a hidden .<partId>.g.part piece to the build cache; source_gen's combining_builder merges all pieces into one .g.dart part. PartBuilder writes its own part extension such as .freezed.dart, and LibraryBuilder writes a standalone importable library.

open as a page