skip to content

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%

answer

  1. two policies, both workable
  2. published packages must ship output
  3. never commit .dart_tool
  4. writes nothing, fails on a diff
  5. 2.16 overwrites hand edits

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.

solid answer

~50 s

Stale output means someone changed a model and did not rerun `build_runner`, or ran a different generator version. build_runner leaves the choice of committing generated files to you, except that a **published** package must include them because pub users cannot run the build. If you commit them, CI should run `dart run build_runner build --only-check` (build_runner 2.16+): it builds, writes and deletes nothing, logs each difference from the files on disk and fails the job. Before 2.16 teams ran a normal build and then `git diff --exit-code`. If you do not commit them, CI must run `dart run build_runner build` before `dart analyze` and tests, and so must every developer after each pull. Either way pin generator versions through `pubspec.lock`, git-ignore `.dart_tool`, and remember that since 2.16 a build overwrites hand edits to generated files unless `--keep-modified-outputs` is passed.

code

yaml · 6 lines
yaml
# CI job steps (generic runner syntax)
steps:
  - run: dart pub get
  - run: dart run build_runner build --only-check
  - run: dart analyze
  - run: dart test

go deeper

for a junior

Recall that generated files must be regenerated after every model change, and that .dart_tool is never committed.

for a middle

Explain both commit policies and what --only-check does differently from a normal build.

for a senior

Design the CI stage: pinned generator versions, --only-check before analysis, and a clear rule for published packages.

for a principal

Weigh reviewable, clone-and-compile repositories against never-stale output across many teams, and set one policy for the monorepo.

## The failure A developer adds `final int age;` to a `@freezed` model that also uses `@JsonSerializable`, updates the constructor, and pushes. They forgot to rerun `build_runner`. The committed `person.freezed.dart` still has the old `copyWith`, and `person.g.dart` still omits `age` from `toJson`. Depending on the change, CI either fails to compile, or, worse, compiles and ships a model that silently drops a field during serialization. The fix is a policy plus a check. ## Policy A: commit generated files Generated `.g.dart` and `.freezed.dart` files are checked in like source. - **For**: the repository compiles right after cloning; reviewers see generated diffs, which can reveal surprising generator behaviour; CI and IDEs need no build step before analysis. - **Against**: noisy diffs; stale output is possible whenever someone forgets to build; merge conflicts in generated files. - **Check**: CI verifies freshness. ## Policy B: generate on every build Generated files are git-ignored and produced by each developer and each CI job. - **For**: output can never be stale in the repository; smaller diffs. - **Against**: every clone, branch switch and CI job pays for a build first; `dart analyze` and tests fail until it runs; onboarding needs one more step. - **Constraint**: this is not an option for a **published pub package**. build_runner's docs state that users getting your package through pub cannot run the build step themselves, so the generated files must be published with it. Neither policy is universally right; the build_runner docs leave it to you. What is wrong is either policy **without** a CI step that enforces it. ## The check: --only-check build_runner **2.16.0** added `--only-check` for continuous builds and presubmits. With it, build_runner: 1. runs the builders as usual; 2. **writes and deletes nothing**; 3. compares what it would have produced with the files on disk; 4. logs each difference and **fails the build** if there is any. ```bash dart pub get dart run build_runner build --only-check dart analyze dart test ``` Before 2.16 the usual recipe was `dart run build_runner build` followed by `git diff --exit-code`, which works but mutates the checkout and depends on git. ## Other causes of drift - **Generator versions**: json_serializable or freezed upgraded on one machine but not another produces different output. Commit `pubspec.lock` for apps so every developer and CI resolve the same versions. - **Formatting**: generators format their output; freezed exposes a `format` option. Differences in formatter settings show up as diffs. - **Hand edits**: since build_runner 2.16 a normal build **fixes incorrect generated files**, undoing manual edits. `--keep-modified-outputs` restores the older behaviour of keeping them until an input or config changes. A hand edit in a committed generated file is therefore both a review smell and something the next build reverts. - **The cache**: `.dart_tool/build` holds internal state and must never be committed; a stale cache is fixed with `dart run build_runner clean`, not by committing it. ## Making it cheap to comply - Document one command (`dart run build_runner build`) and run `watch` while editing models. - Restrict generators with `generate_for` so a full build stays short. - Put `--only-check` in the same CI stage as analysis, so the failure message points straight at the stale file. - In a workspace, `--workspace` builds or checks all packages in one invocation. ## Choosing, as a team | Question | Leans to committing | Leans to generating | |---|---|---| | Is the package published to pub? | required | not possible | | Do reviewers need to see generated diffs? | yes | no | | Must a fresh clone compile before any build? | yes | no | | Do developers often forget to rebuild? | add `--only-check` | output never stale in git | Whatever you choose, write it down in the repository's contributing notes, and make the CI failure message say exactly which command fixes it. A red build that tells the author "run `dart run build_runner build` and commit the result" is fixed in a minute; one that fails later inside a test with a missing field can cost an afternoon.

  • Why is --only-check better than building in CI and running git diff --exit-code?
    It does not modify the checkout, it reports differences in build_runner's own log with the affected outputs, and it does not depend on git being present or clean. The git-diff recipe still works on build_runner versions before 2.16.
  • Your team does not commit generated files; what must change in CI and onboarding?
    Every CI job runs `dart run build_runner build` after `dart pub get` and before analysis or tests, and every developer does the same after cloning or pulling model changes. `.g.dart` and `.freezed.dart` go in `.gitignore`. It also rules out publishing that package to pub without a step that generates the files first.

saying these in an interview costs you the question

  • Commits .dart_tool/build so CI can skip the build step
  • Believes pub runs build_runner for consumers of a published package
  • Relies on developers remembering to rebuild, with no CI check
  • Fixes a stale .g.dart by editing it by hand
  • Thinks --only-check writes fresh outputs and then compares them