Irbisa · Cheatsheet

Code Generation & build_runner

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

Middle Developer · 18 questions

  1. 01

    What is build_runner, and why does Dart generate code at build time instead of using runtime reflection?

    build_runner is the dev-time tool that runs code generators (builders) over your package and writes plain Dart files such as user.g.dart.

  2. 02

    Why does a json_serializable model need part 'user.g.dart'; and the forwarders to _$UserFromJson?

    The generated functions are private (they start with _), so they must live in the same library as your class.

    import 'package:json_annotation/json_annotation.dart';import 'address.dart'; // the part file cannot import anything itself part 'user.g.dart'; // you write this; the generator writes `part of` @JsonSerializable()…
  3. 03

    What is the difference between build_runner build, watch and clean, and what does --delete-conflicting-outputs do today?

    build generates once and exits, watch keeps running and regenerates only what changed on each save, and clean deletes the build cache in .dart_tool/build.

    # one-off generation: after a pull, a dependency upgrade, in CIdart run build_runner build # day-to-day: stays running, rebuilds only what changeddart run build_runner watch…
  4. 04

    How do you control key names and enum values with json_serializable: snake_case, one renamed field, a skipped field, an unknown enum value?

    With annotations: fieldRename on @JsonSerializable renames every key, @JsonKey(name: ...) overrides one field, includeFromJson/includeToJson: false leaves a field out, and @JsonValue plus unknownEnumValue map enum values safely.

    enum Status {  @JsonValue('in_progress')  inProgress,  done,  unknown,}…
  5. 05

    Should generated .g.dart and .freezed.dart files be committed to git?

    For an app, usually not: gitignore them and generate them after every clone and in CI.

  6. 06

    What does print(order.toJson()) show here, and why can that break code that does not go through jsonEncode?

    @JsonSerializable()class Order {  const Order(this.id, this.address);  final String id;  final Address address; // Address is @JsonSerializable too   Map<String, dynamic> toJson() => _$OrderToJson(this);} void main() {  final order = Order('42', const Address('Almaty'));  print(order.toJson());  print(jsonEncode(order.toJson()));}

    It prints {id: 42, address: Instance of 'Address'}: by default the generated toJson puts the nested object itself into the map.

  7. 07

    What happens when each of these three payloads is passed to the generated Profile.fromJson?

    @JsonSerializable()class Profile {  const Profile({required this.name, required this.age, this.bio});  final String name;  final int age;  final String? bio;   factory Profile.fromJson(Map<String, dynamic> json) =>      _$ProfileFromJson(json);} Profile.fromJson({'name': 'Ann', 'age': 30.0}); // 1Profile.fromJson({'name': null, 'age': 30}); // 2Profile.fromJson({'name': 'Ann', 'age': '30'}); // 3

    The first works, the other two throw a TypeError.

  8. 08

    How do you serialize a type json_serializable cannot handle, such as an epoch-millis timestamp or a Money value object?

    Write a JsonConverter<T, Json> with fromJson and toJson, and attach it to the field, to the class, or through converters: in @JsonSerializable.

    class Money {  const Money(this.cents);  final int cents; // minor units, never a double} class MoneyConverter implements JsonConverter<Money, int> {…
  9. 09

    Find the bug: a user edits only their name on the profile screen, and their bio and avatar disappear on the server.

    // PATCH /me: the server updates only the keys present in the body@JsonSerializable(fieldRename: FieldRename.snake)class ProfilePatch {  const ProfilePatch({this.name, this.bio, this.avatarUrl});  final String? name;  final String? bio;  final String? avatarUrl;   Map<String, dynamic> toJson() => _$ProfilePatchToJson(this);} // Edit profile screen: the user changed only the nameFuture<void> rename(Dio dio, String name) =>    dio.patch<void>('/me', data: ProfilePatch(name: name).toJson());

    The generated toJson writes every field, null included, so the body is {"name":"Ann","bio":null,"avatar_url":null}.

  10. 10

    What does freezed generate for you, and when is a plain sealed class or @JsonSerializable enough?

    freezed generates field-by-field ==/hashCode, toString, a copyWith that can also set a field to null, and the classes behind factory constructors.

    import 'package:freezed_annotation/freezed_annotation.dart'; part 'user.freezed.dart'; // ==, hashCode, toString, copyWithpart 'user.g.dart'; // fromJson / toJson, from json_serializable @freezed…
  11. 11

    This model compiled with freezed 2, but after upgrading to freezed 3 it fails with "Missing concrete implementations". What changed?

    import 'package:freezed_annotation/freezed_annotation.dart'; part 'point.freezed.dart'; @freezedclass Point with _$Point {  const factory Point({required double x, required double y}) = _Point;} // error: Missing concrete implementations of// 'getter mixin _$Point on Object.x' and 'getter mixin _$Point on Object.y'.

    Freezed 3 requires the annotated class to be abstract (a single data class) or sealed (a union).

  12. 12

    Find the bug: the round-trip tests for these events pass, but in production every screen_view event fails to decode.

    // Server events: {"type": "purchase", "sku": "A1", "cents": 990}//                {"type": "screen_view", "route": "/home"}@Freezed(unionKey: 'type')sealed class Event with _$Event {  const factory Event.purchase({required String sku, required int cents}) =      Purchase;  const factory Event.screenView({required String route}) = ScreenView;   factory Event.fromJson(Map<String, dynamic> json) => _$EventFromJson(json);}

    Freezed uses the constructor name as the union value, so the generated fromJson matches 'screenView', never 'screen_view'.

  13. 13

    What is build.yaml used for, and how do you limit which files a builder processes?

    build.yaml at the package root configures the builders for that package: which files each one reads (generate_for), its options, and whether it runs at all (enabled).

    # build.yaml, next to pubspec.yamltargets:  $default:    builders:      json_serializable:        generate_for:…
  14. 14

    You add a string to an ARB file and run build_runner, but AppLocalizations gets no new getter. Why?

    ARB files are not part of the build_runner pipeline.

    # l10n.yaml at the project root: read by `flutter gen-l10n`, not build_runnerarb-dir: lib/l10n/arbtemplate-arb-file: app_en.arboutput-dir: lib/l10n # real source files, imported normallyoutput-localization-file: app_localizations.dartoutput-class: AppLocalizations…
  15. 15

    A full build_runner build takes several minutes. Where does the time go, and how do you bring it down?

    Mostly into the analyzer resolving libraries for the builders: each annotated file is resolved together with everything it imports.

  16. 16

    How would you write your own code generator with source_gen, and how does build_runner pick it up?

    Subclass GeneratorForAnnotation<T> and return the code to emit from generateForAnnotatedElement, wrap it in a SharedPartBuilder inside a top-level factory function, and declare that factory in the generator package's build.yaml.

    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'; // AnalyticsEvent class EventGenerator extends GeneratorForAnnotation<AnalyticsEvent> {…
  17. 17

    In a monorepo, why does build_runner generate nothing for a feature package, and how do you set up generation across packages?

    A builder applies only to packages that depend on its package directly (auto_apply: dependents), and a plain run writes source outputs only for the package it runs in.

    # pubspec.yaml at the repo root: a pub workspacename: shop_workspaceenvironment:  sdk: ^3.9.0workspace:  - app…
  18. 18

    What happened to Dart macros, and what does that mean for code generation in your project?

    The Dart team cancelled macros in January 2025 before they shipped: running user code inside the compiler and analyzer on every keystroke cost too much in IDE speed and hot reload.