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#
| File | Purpose |
|---|---|
lib/builder.dart |
ONLY exports BuilderFactory functions — this is the import in build.yaml |
lib/my_generator.dart | Optional: public API for any runtime utilities |
lib/src/generator.dart | The Generator implementation — private |
lib/src/utils.dart | Type helpers, string utilities, code helpers |
lib/src/annotation_reader.dart | Centralized annotation reading logic |
build.yaml | build_runner registration — required |
pubspec.yaml | Package 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)