LogoDart Code Generation

Generator Package Structure#

Patterns derived from json_serializable, dart_mappable, and retrofit source packages.


Standard Two-Package Layout#

Most production generators split into two packages:

my_generator/              ← dev_dependency (users add to dev_dependencies)
  lib/
    builder.dart           ← BuilderFactory function (the entry point)
    src/
      generator.dart       ← Extends GeneratorForAnnotation<T>
      utils.dart           ← Shared utilities
  build.yaml               ← Declares builders to build_runner
  pubspec.yaml

my_annotations/            ← regular dependency (users add to dependencies)
  lib/
    my_annotations.dart    ← Public API
    src/
      my_annotation.dart   ← Annotation class definitions
  pubspec.yaml

The split exists because:

  • Users need the annotation classes at compile time (runtime dependency)
  • Users only need the generator during code generation (dev dependency)
  • The generator can have heavy deps (analyzer, build) that shouldn't pollute user deps

Single-Package Layout (simpler)#

Some generators (like retrofit) combine both in one package:

my_generator/
  lib/
    my_generator.dart       ← Exports both annotations and builder factory
    builder.dart            ← BuilderFactory (or in my_generator.dart)
    src/
      annotations.dart      ← Annotation classes
      generator.dart        ← Generator implementation
  build.yaml
  pubspec.yaml

Users then have:

dev_dependencies:
  my_generator: ^1.0.0

And import package:my_generator/my_generator.dart for annotations.


Workspace Layout (Monorepo)#

For complex generators (like dart_mappable and json_serializable), use Dart's workspace feature:

my_workspace/
  pubspec.yaml              ← workspace: {packages: [...]}
  packages/
    my_annotations/
      pubspec.yaml          ← resolution: workspace
      lib/...
    my_generator/
      pubspec.yaml          ← resolution: workspace
      lib/...
      build.yaml
  examples/
    basic_example/
      pubspec.yaml
      lib/...
      build.yaml

Workspace pubspec.yaml#

name: my_workspace
publish_to: none

environment:
  sdk: '>=3.7.0 <4.0.0'

workspace:
  - packages/my_annotations
  - packages/my_generator
  - examples/basic_example

Package pubspec.yaml in workspace#

name: my_generator
resolution: workspace   # ← Required in all workspace members

Generator pubspec.yaml#

name: my_generator
version: 1.0.0
description: Code generator for my_annotations.
repository: https://github.com/user/my_generator

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  # Core build system
  build: ^4.0.0

  # Source code analysis
  analyzer: '>=8.0.0 <13.0.0'    # Wide range — don't be too strict
  # Note: analyzer API is stable but version changes fast
  # Match what your deps use: source_gen, build_runner, etc.

  # Higher-level generator framework
  source_gen: ^4.0.0

  # Code formatting
  dart_style: ^3.0.0

  # The annotation package (your own or external)
  my_annotations: ^1.0.0

  # Optional: for code_builder approach
  code_builder: ^4.10.0

  # Optional: for YAML config parsing
  yaml: ^3.1.0

  # Optional: for path manipulation
  path: ^1.9.0

  # Optional: for pub version checking
  pub_semver: ^2.1.0

dev_dependencies:
  build_runner: ^2.10.0
  build_test: ^3.3.0
  test: ^1.25.0
  lints: ^4.0.0
  # For generator tests:
  source_gen_test: ^1.3.0

Annotations pubspec.yaml#

name: my_annotations
version: 1.0.0
description: Annotations for my_generator.

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  meta: ^1.9.0       # For @Target (optional but recommended)

# No dev_dependencies needed for pure annotations package

lib/builder.dart — The Entry Point#

This file is what build.yaml imports. It must expose a BuilderFactory function:

// lib/builder.dart
library;

import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';

import 'src/my_generator.dart';

/// Entry point for build_runner.
/// Function name must match build.yaml builder_factories list.
Builder myBuilder(BuilderOptions options) {
  // Parse options from build.yaml
  final someOption = options.config['some_option'] as String? ?? 'default';
  final anotherOption = options.config['another_option'] as bool? ?? false;

  return SharedPartBuilder(
    [MyGenerator(someOption: someOption, anotherOption: anotherOption)],
    'my_builder',  // This becomes the suffix: .my_builder.g.part
  );
}

Multiple builders in one package#

// lib/builder.dart

/// Main builder for @MyAnnotation on classes
Builder myClassBuilder(BuilderOptions options) => SharedPartBuilder(
  [MyClassGenerator(options)],
  'my_class',
);

/// Secondary builder for @MyEnum on enums
Builder myEnumBuilder(BuilderOptions options) => SharedPartBuilder(
  [MyEnumGenerator(options)],
  'my_enum',
);
# build.yaml
builders:
  my_class_builder:
    import: "package:my_generator/builder.dart"
    builder_factories: ["myClassBuilder"]
    build_extensions: {".dart": [".my_class.g.part"]}
    auto_apply: dependents
    build_to: cache
    applies_builders: ["source_gen|combining_builder"]

  my_enum_builder:
    import: "package:my_generator/builder.dart"
    builder_factories: ["myEnumBuilder"]
    build_extensions: {".dart": [".my_enum.g.part"]}
    auto_apply: dependents
    build_to: cache
    applies_builders: ["source_gen|combining_builder"]

lib/src/generator.dart — The Generator#

// lib/src/generator.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 MyGenerator extends GeneratorForAnnotation<MyAnnotation> {
  const MyGenerator({
    this.someOption = 'default',
    this.anotherOption = false,
  });

  final String someOption;
  final bool anotherOption;

  @override
  String generateForAnnotatedElement(
    Element element,
    ConstantReader annotation,
    BuildStep buildStep,
  ) {
    if (element is! ClassElement) {
      throw InvalidGenerationSourceError(
        '@MyAnnotation can only be applied to classes.',
        element: element,
        todo: 'Remove @MyAnnotation from ${element.displayName}.',
      );
    }

    return _generateFor(element, annotation);
  }

  String _generateFor(ClassElement element, ConstantReader annotation) {
    // Read annotation fields
    final name = annotation.peek('name')?.stringValue ?? element.name;

    // Generate code
    return '''
extension \$${element.name}Extension on ${element.name} {
  String get generatedName => '$name';
}
''';
  }
}

Directory Structure — What Goes Where#

FilePurpose
lib/builder.dart ONLY exports BuilderFactory functions — this is the import in build.yaml
lib/my_generator.dartOptional: public API for any runtime utilities
lib/src/generator.dartThe Generator implementation — private
lib/src/utils.dartType helpers, string utilities, code helpers
lib/src/annotation_reader.dartCentralized annotation reading logic
build.yamlbuild_runner registration — required
pubspec.yamlPackage metadata and dependencies
test/Tests using build_test
example/Example package showing generator usage

How Users Consume Your Generator#

pubspec.yaml (user's package)#

dependencies:
  my_annotations: ^1.0.0    # Runtime annotations (or combined package)

dev_dependencies:
  my_generator: ^1.0.0      # Code generator
  build_runner: ^2.10.0     # Required to run generators

Running the generator#

dart run build_runner build           # One-time build
dart run build_runner watch           # Watch mode
dart run build_runner build --delete-conflicting-outputs  # Force rebuild

User's source file#

// lib/model.dart
import 'package:my_annotations/my_annotations.dart';

part 'model.g.dart';  // Required for SharedPartBuilder

@MyAnnotation(name: 'my_model')
class Model {
  final String id;
  final String name;
  Model({required this.id, required this.name});
}

Optional: user's build.yaml for configuration#

# User's build.yaml
global_options:
  "my_generator|my_builder":
    options:
      some_option: custom_value

Analyzer Version Compatibility#

The analyzer package versions change frequently and often break APIs:

# Recommended: wide range to maximize compatibility
analyzer: '>=8.0.0 <13.0.0'    # dart_mappable style (wide lower bound)
analyzer: '>=10.0.0 <13.0.0'   # json_serializable style (tighter lower bound)
analyzer: '>=8.0.0 <13.0.0'    # build_runner 2.13.x requires this range

Key insight: specify a range, not an exact version. The lower bound is the version you tested against, the upper bound prevents accidental breakage from major changes.


Keeping Generator and Annotations in Sync#

If both packages are versioned together (like dart_mappable's 4.7.0/4.7.0), constrain the dependency tightly:

# my_generator/pubspec.yaml
dependencies:
  my_annotations: '>=1.0.0 <2.0.0'  # Same major version

If the generator reads annotation fields that changed, it will fail to generate. Tight version constraints prevent mismatches.


Complete File Template Summary#

Minimum files for a working generator:

my_generator/
  lib/
    builder.dart           ← Builder myBuilder(BuilderOptions) => SharedPartBuilder([MyGenerator()], 'my')
    src/
      generator.dart       ← class MyGenerator extends GeneratorForAnnotation<MyAnnotation>
  build.yaml               ← builders: my_builder: import/factories/extensions/auto_apply/build_to/applies_builders
  pubspec.yaml             ← deps: build, analyzer, source_gen, dart_style, my_annotations

my_annotations/
  lib/
    my_annotations.dart    ← export 'src/my_annotation.dart'
    src/
      my_annotation.dart   ← @Target(...) class MyAnnotation { const MyAnnotation(); }
  pubspec.yaml             ← deps: meta (optional)