Code Generation & build_runner
Irbisa · cheatsheetSeptember 13, 2026

Code Generation & build_runner

build_runner, part files, json_serializable, freezed, build.yaml, source_gen, build times

Middle Developer16 itemscompressed for a skim
  1. 01

    Your model declares part 'user.g.dart'; and the generated factory is _$UserFromJson — why a part file instead of a plain import?

    Easy

    Because the generated code is private to your library and reaches into your class: a part shares the library's scope, so _$UserFromJson can start with an underscore and still be visible from user.dart.

    // lib/models/user.dart — the library
    import 'package:json_annotation/json_annotation.dart';
    import 'address.dart';        // the part file cannot import this itself
    
    part 'user.g.dart';           // ✅ you write this; the generator never adds it
    …
  2. 02

    Half the team types build_runner build --delete-conflicting-outputs by reflex — what conflict is that flag actually deleting?

    Easy

    A build writes a file only if build_runner already owns it, and --delete-conflicting-outputs (-d) is you granting permission to take over generated files that exist on disk but are missing from its asset graph.

    # ── the day-to-day loop: incremental, stays running ────────────
    dart run build_runner watch --delete-conflicting-outputs
    
    # ── after a pull, a branch switch or a dependency upgrade ──────
    dart run build_runner build --delete-conflicting-outputs   # -d for short
    …
  3. 03

    A reviewer complains that your twelve-file PR is four thousand lines because of .g.dart — should generated code be in git at all?

    Easy

    For an application, no — ignore it and generate it in CI; for a package you publish to pub.dev, yes, because dart pub publish builds its archive from tracked files and your consumers do not run your builders.

    # ── App repo: generated code is a build artefact ──────────────
    $ cat .gitignore
    .dart_tool/
    *.g.dart
    *.freezed.dart
    *.mocks.dart
    …
  4. 04

    Calling toJson() on an order puts an Address object inside the map instead of a nested map — which json_serializable option did you forget?

    Medium

    explicitToJson: true.

    @JsonSerializable(explicitToJson: true, fieldRename: FieldRename.snake)
    class Order {
      const Order({
        required this.id,
        required this.address,
        required this.items,
    …
  5. 05

    The API sends timestamps as epoch millis and money as integer minor units — how do you teach json_serializable about a type it cannot decode?

    Medium

    Write a JsonConverter<Dart, Wire> and hang it on the field or the class; the generator then calls your fromJson/toJson instead of emitting a cast.

    class Money {
      const Money(this.minorUnits, this.currency);
      final int minorUnits;
      final String currency;
    }
    …
  6. 06

    One payload arrives with bio missing, name explicitly null and age as the string "30" — what does the generated fromJson do with each?

    Medium

    It casts and nothing else, so a missing key and an explicit null are indistinguishable, and both name and age throw a TypeError that does not say which key failed.

    @JsonSerializable(checked: true)
    class Profile {
      const Profile({required this.name, required this.age, this.bio = ''});
    
      final String name;
    …
  7. 07

    A reviewer asks why a ten-field @freezed model produced three hundred lines in user.freezed.dart — what is all of that, and what does it cost?

    Medium

    Everything you would otherwise hand-write for a value type: a mixin carrying ==, hashCode, toString and the entry point for copyWith, the concrete class the factory redirects to, and a copyWith helper class per model.

    // lib/models/user.dart
    import 'package:freezed_annotation/freezed_annotation.dart';
    
    part 'user.freezed.dart';   // copyWith, ==, hashCode, toString
    part 'user.g.dart';         // fromJson/toJson, from json_serializable
    …
  8. 08

    Dart 3 gives you sealed classes, exhaustive switches and destructuring for free — what is left for a freezed union in 2026?

    Medium

    Only the members the language still will not write for you: value equality, a per-variant copyWith and JSON with a discriminator — the matching itself is identical now, because freezed 3 dropped when/map in favour of pattern matching.

    // ── Hand-written: no build step, exhaustive by construction ───────
    sealed class LoadState {
      const LoadState();
    }
    
    final class Idle extends LoadState { const Idle(); }
    …
  9. 09

    Only ten files in your app are models, but every build_runner run touches all of lib/ — what goes in build.yaml to narrow that?

    Medium

    Scope each builder with generate_for inside a target, so its inputs are a glob of the files that actually carry annotations instead of the package default of lib/**.

    # build.yaml — next to pubspec.yaml, at the package root
    targets:
      $default:
        builders:
          # short key works when package and builder share a name;
          # the full form is json_serializable:json_serializable
    …
  10. 10

    A full build_runner run on your app now takes four minutes. Where is that time actually going, and which fix wins the most back?

    Medium

    Almost all of it is the analyzer resolving Dart libraries on behalf of builders, so the wins come from letting fewer files reach fewer builders and from never throwing the asset graph away.

    # Cold: no asset graph, every input hashed, every applied builder resolved.
    $ dart run build_runner build --delete-conflicting-outputs
    [INFO] Succeeded after 3m41s with 412 outputs
    
    # Same command again — .dart_tool/build survived, so only changed inputs run.
    $ dart run build_runner build
    …
  11. 11

    A new hire clones the repo, opens the IDE and sees 200 analyzer errors in files nobody has touched. What is wrong, and how do you stop it happening?

    Medium

    Nothing is wrong with the code — the .g.dart and .freezed.dart parts are gitignored and have not been generated yet, so every part directive dangles and everything that depends on a generated symbol fails with it.

    #!/usr/bin/env bash
    # tool/bootstrap.sh — the one command in the README, and the same one CI runs.
    set -euo pipefail
    
    dart pub get
    …
  12. 12

    Your team wants a one-annotation generator for analytics event classes. What do you actually have to write and register for build_runner to run it?

    Medium

    Three pieces: a Generator (in practice GeneratorForAnnotation<T>), a top-level factory function that wraps it in a SharedPartBuilder or LibraryBuilder, and a builders: entry in build.yaml naming that function.

    // package:my_gen/builder.dart
    import 'package:analyzer/dart/element/element.dart';
    import 'package:build/build.dart';
    import 'package:source_gen/source_gen.dart';
    import 'package:my_annotations/my_annotations.dart'; // class AnalyticsEvent
    …
  13. 13

    In a melos monorepo, feature_cart has annotated models but nothing is generated for it, while the app package is fine. What is wrong with the graph?

    Hard

    feature_cart almost certainly has no dev_dependency on the generator package, and even once it does, build_runner writes generated sources only into the package it is run in — so the fix is an edge per package plus a build per package.

    # packages/core_annotations/pubspec.yaml — pure Dart, ships in the app
    name: core_annotations
    environment: {sdk: ^3.9.0}
    # no analyzer, no build, no source_gen here
    
    ---
    …
  14. 14

    Dart macros were supposed to make build_runner unnecessary and then got cancelled. What are you actually building on today, and what do augmentations change?

    Hard

    build_runner and source_gen, unchanged — macros were dropped in early 2025 without ever shipping, and the piece of that work that survived is augmentations, which changes what generated code looks like, not how it is produced.

    // ── Today: the mixin-and-free-function dance ─────────────────────────────
    // user.dart
    import 'package:json_annotation/json_annotation.dart';
    part 'user.g.dart';
    
    @JsonSerializable()
    …
  15. 15

    You add a string to app_en.arb, run build_runner, and AppLocalizations still has no getter for it. Why not?

    Hard

    Because localization is not part of the build_runner pipeline at all — AppLocalizations is written by flutter gen-l10n, a subcommand of the Flutter tool driven by l10n.yaml, and build_runner has never heard of your ARB files.

    # l10n.yaml — read by `flutter gen-l10n`, NOT by build_runner
    arb-dir: lib/l10n/arb
    template-arb-file: app_en.arb
    output-localization-file: app_localizations.dart
    output-class: AppLocalizations
    output-dir: lib/l10n              # a real directory, not package:flutter_gen
    …
  16. 16

    Generated files are committed and half the PRs carry a stale .g.dart. How do you make CI catch that without asking reviewers to read generated diffs?

    Hard

    Regenerate in CI and fail on any change to the working tree, then mark the generated files linguist-generated so nobody has to look at them in the pull request.

    # .github/workflows/codegen.yaml
    name: codegen
    on: pull_request
    
    jobs:
      drift:
    …