source_gen Reference#
Source of truth: used directly by json_serializable (v6.13.1) and retrofit_generator (v10.2.3). Package:
source_gen: ^4.2.0(version 4.2.2 in this workspace)
source_gen is a higher-level framework built on top of build and analyzer. It provides:
-
Generator/GeneratorForAnnotation— produce Dart code for annotated elements LibraryReader— convenient iteration over library elementsConstantReader— safe access to annotation constant valuesTypeChecker— type identity and hierarchy checking-
Pre-built builder types:
SharedPartBuilder,PartBuilder,LibraryBuilder
Generator#
Base class for any code generator:
abstract class Generator {
/// Called for each library. Return the generated code as a String,
/// or null/empty to produce nothing.
FutureOr<String?> generate(LibraryReader library, BuildStep buildStep);
}
You rarely extend Generator directly — use GeneratorForAnnotation instead.
GeneratorForAnnotation<T>#
The standard pattern for annotation-driven generators:
abstract class GeneratorForAnnotation<T> extends Generator {
@override
Future<String> generate(LibraryReader library, BuildStep buildStep) async {
// source_gen calls generateForAnnotatedElement for each annotated element
// and joins the results
}
/// Override this in your generator.
/// Called once per element annotated with T in the library.
FutureOr<String> generateForAnnotatedElement(
Element element,
ConstantReader annotation, // The @T annotation value on this element
BuildStep buildStep,
);
}
Minimal complete example#
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> {
@override
String generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) {
// Validate the element type
if (element is! ClassElement) {
throw InvalidGenerationSourceError(
'@MyAnnotation can only be applied to classes.',
element: element,
todo: 'Remove @MyAnnotation from ${element.displayName}.',
);
}
final className = element.name;
final prefix = annotation.peek('prefix')?.stringValue ?? '';
return '''
extension \$${className}Extension on $className {
String get label => '${prefix}\$${className}';
}
''';
}
}
Return type variations#
generateForAnnotatedElement can return:
String— code to include in the outputIterable<String>— multiple code pieces (joined with\n\n)Future<String>— async generationnull— generate nothing for this element
json_serializable uses Iterable<String> via sync*:
@override
Iterable<String> generateForAnnotatedElement(
Element element,
ConstantReader annotation,
BuildStep buildStep,
) sync* {
yield _generateFromJson(element);
if (config.createToJson) {
yield _generateToJson(element);
}
}
Important: the inPackage constructor parameter#
When using GeneratorForAnnotation<T> where T is in a different package, specify:
class MyGenerator extends GeneratorForAnnotation<MyAnnotation> {
MyGenerator() : super(inPackage: 'my_annotations');
// This tells source_gen which package the annotation type lives in
}
json_serializable does this:
class JsonSerializableGenerator extends GeneratorForAnnotation<JsonSerializable> {
JsonSerializableGenerator.fromSettings(this._settings)
: super(inPackage: 'json_annotation');
}
LibraryReader#
Wraps LibraryElement with convenience methods:
class LibraryReader {
final LibraryElement element;
LibraryReader(this.element);
/// Returns a top-level ClassElement visible by [name], following export directives.
ClassElement? findType(String name);
/// The library element and all its direct children (NOT recursive through exports).
Iterable<Element> get allElements; // [element, ...element.children]
/// Top-level class elements in this library (not including enums).
Iterable<ClassElement> get classes;
/// Top-level enum elements in this library.
Iterable<EnumElement> get enums;
/// All top-level elements annotated with [checker] (assignable match).
Iterable<AnnotatedElement> annotatedWith(
TypeChecker checker, {
bool throwOnUnresolved = true,
});
/// All top-level elements annotated with exactly [checker] (no subclasses).
Iterable<AnnotatedElement> annotatedWithExact(
TypeChecker checker, {
bool throwOnUnresolved = true,
});
/// All library directives (imports, exports, part includes) annotated with [checker].
Iterable<AnnotatedDirective> libraryDirectivesAnnotatedWith(
TypeChecker checker, {
bool throwOnUnresolved = true,
});
/// URL helpers
Uri pathToAsset(AssetId asset);
Uri pathToElement(Element element);
Uri pathToUrl(dynamic toUrlOrString);
}
class AnnotatedElement {
final Element element;
final ConstantReader annotation; // The annotation value
}
class AnnotatedDirective {
final ElementDirective directive;
final ConstantReader annotation;
}
LibraryReader usage#
// In a raw Generator (not GeneratorForAnnotation):
@override
String generate(LibraryReader library, BuildStep buildStep) {
const checker = TypeChecker.typeNamed(MyAnnotation);
final buffer = StringBuffer();
for (final annotated in library.annotatedWith(checker)) {
final classElement = annotated.element as ClassElement;
final annotation = annotated.annotation; // ConstantReader
buffer.writeln(_generateFor(classElement, annotation));
}
return buffer.toString();
}
TypeChecker#
Checks type relationships using the analyzer's type system. All comparisons are structurally correct — they handle the analyzer's internal type representations.
abstract class TypeChecker {
/// From an already-resolved DartType
const factory TypeChecker.fromStatic(DartType type) = _LibraryTypeChecker;
/// From a library URL (brittle — prefer typeNamed for most cases)
/// Example: TypeChecker.fromUrl('dart:collection#LinkedHashMap')
const factory TypeChecker.fromUrl(dynamic url) = _UriTypeChecker;
/// Match any type with the same name as [type].
/// Pass [inPackage] to restrict to a specific package; set [inSdk] for dart: packages.
/// This is the preferred replacement for the removed TypeChecker.fromRuntime().
const factory TypeChecker.typeNamed(
Type type, {
String? inPackage,
bool? inSdk,
}) = _NameTypeChecker;
/// Match any type with exactly the string [name] (no Type object needed).
/// Same optional [inPackage] / [inSdk] filtering as typeNamed.
const factory TypeChecker.typeNamedLiterally(
String name, {
String? inPackage,
bool? inSdk,
}) = _LiteralNameTypeChecker;
/// Returns true if ANY of the given checkers match (logical OR).
const factory TypeChecker.any(Iterable<TypeChecker> checkers) = _AnyChecker;
}
TypeChecker methods#
final checker = TypeChecker.typeNamed(SomeClass);
// --- Element-based checks ---
bool isExactly(Element element); // element is exactly SomeClass
bool isAssignableFrom(Element element); // element is SomeClass or subtype
bool isSuperOf(Element element); // element is supertype of SomeClass
// --- Type-based checks ---
bool isExactlyType(DartType type);
bool isAssignableFromType(DartType type);
bool isSuperTypeOf(DartType type);
// --- Annotation presence checks ---
bool hasAnnotationOf(Element element, {bool throwOnUnresolved = true});
bool hasAnnotationOfExact(Element element, {bool throwOnUnresolved = true});
// --- Getting annotation DartObjects ---
/// Get all @SomeClass annotations on element (assignable match)
Iterable<DartObject> annotationsOf(
Object element, { // accepts Element or ElementDirective
bool throwOnUnresolved = true,
});
/// Get the first @SomeClass annotation, or null (assignable match)
DartObject? firstAnnotationOf(
Object element, { // accepts Element or ElementDirective
bool throwOnUnresolved = true,
});
/// Get all @SomeClass annotations (exact type match, no subclasses)
Iterable<DartObject> annotationsOfExact(
Element element, {
bool throwOnUnresolved = true,
});
/// Get the first @SomeClass annotation (exact match), or null
DartObject? firstAnnotationOfExact(
Element element, {
bool throwOnUnresolved = true,
});
TypeChecker examples#
// Check annotation presence (annotation in a user/external package)
const myChecker = TypeChecker.typeNamed(MyAnnotation);
if (myChecker.hasAnnotationOf(element)) {
final annotation = myChecker.firstAnnotationOf(element)!;
// Read annotation.getField('name')?.toStringValue()
}
// Restrict check to a specific package (avoids false positives from same-named types)
const myChecker = TypeChecker.typeNamed(MyAnnotation, inPackage: 'my_annotations');
// Check type hierarchy for an annotation from another package
const listChecker = TypeChecker.typeNamed(List, inPackage: 'core', inSdk: true);
if (listChecker.isAssignableFromType(field.type)) {
// field.type is List or a subtype of List
}
// SDK type check — always pass inPackage + inSdk for dart: types
const futureChecker = TypeChecker.typeNamed(Future, inPackage: 'async', inSdk: true);
if (futureChecker.isExactlyType(method.returnType)) {
// return type is exactly Future<something>
}
// Match by string name when you don't have a Type reference
const dioChecker = TypeChecker.typeNamedLiterally('Dio', inPackage: 'dio');
ConstantReader#
Safe access to the constant value of an annotation. Provided as annotation parameter in
generateForAnnotatedElement, or constructed from a DartObject.
class ConstantReader {
ConstantReader(DartObject? value);
// --- Type checks ---
bool get isNull;
bool get isString;
bool get isInt;
bool get isDouble;
bool get isBool;
bool get isList;
bool get isSet;
bool get isMap;
bool get isType; // Is a Type value (e.g. String, int)
bool get isSymbol;
bool get isLiteral; // Can be represented as a Dart literal
// --- Value extraction ---
// These throw if the value is null or the wrong type
String get stringValue;
int get intValue;
double get doubleValue;
bool get boolValue;
List<DartObject> get listValue;
Set<DartObject> get setValue;
Map<DartObject?, DartObject?> get mapValue;
DartType get typeValue; // For Type fields
Symbol get symbolValue;
// --- Field access ---
/// Get a field by name. Returns null reader if field is null or not found.
ConstantReader? peek(String field);
/// Get a field by name. Throws if field is null.
ConstantReader read(String field);
// --- Raw object ---
DartObject get objectValue;
// --- Reviver (for non-literal values) ---
/// Returns a Reviver that can reconstruct the value in source code.
/// Use .accessor for enum names, .arguments for constructor calls.
Reviver revive();
}
Reading annotation fields#
// @MyAnnotation(name: 'hello', count: 42, enabled: true, tags: ['a', 'b'])
String generateForAnnotatedElement(
Element element,
ConstantReader annotation, // This is the ConstantReader for @MyAnnotation
BuildStep buildStep,
) {
// peek returns null if field doesn't exist or is null
final name = annotation.peek('name')?.stringValue ?? 'default';
final count = annotation.peek('count')?.intValue ?? 0;
final enabled = annotation.peek('enabled')?.boolValue ?? false;
// List field
final tags = annotation.peek('tags')?.listValue
.map((obj) => obj.toStringValue()!)
.toList() ?? [];
// Map field
final headers = annotation.peek('headers')?.mapValue.map((k, v) =>
MapEntry(k?.toStringValue() ?? '', v?.toStringValue() ?? ''));
// Type field (e.g. @MyAnnotation(type: MyClass))
final typeArg = annotation.peek('type')?.typeValue;
// Enum field (e.g. @MyAnnotation(mode: Mode.fast))
// .revive().accessor gives 'fast', .revive().source gives 'Mode.fast'
final enumStr = annotation.peek('mode')?.revive().accessor;
final mode = Mode.values.firstWhere(
(e) => e.name == enumStr,
orElse: () => Mode.normal,
);
// Nested annotation field
final nestedAnnotation = annotation.peek('nested');
if (nestedAnnotation != null && !nestedAnnotation.isNull) {
final nestedValue = nestedAnnotation.peek('value')?.stringValue;
}
}
Reading from DartObject directly (outside GeneratorForAnnotation)#
When you have a DartObject (e.g., from TypeChecker.firstAnnotationOf()):
final DartObject? obj = myChecker.firstAnnotationOf(element);
if (obj != null) {
final name = obj.getField('name')?.toStringValue();
final count = obj.getField('count')?.toIntValue();
final enabled = obj.getField('enabled')?.toBoolValue();
final list = obj.getField('items')?.toListValue();
final map = obj.getField('config')?.toMapValue();
final type = obj.getField('type')?.toTypeValue();
// For enums: get the variable element and its name
final enumField = obj.getField('mode');
final enumName = enumField?.variable?.name; // 'fast', 'slow', etc.
}
Builder Types in source_gen#
SharedPartBuilder#
The most common type for annotation-driven generators. Outputs .name.g.part files that are merged into a single
.g.dart by source_gen|combining_builder.
// lib/builder.dart
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'src/my_generator.dart';
Builder myBuilder(BuilderOptions options) => SharedPartBuilder(
[MyGenerator()],
'my_builder', // Unique name for this builder — output is .my_builder.g.part
);
# build.yaml
builders:
my_builder:
import: "package:my_package/builder.dart"
builder_factories: ["myBuilder"]
build_extensions: {".dart": [".my_builder.g.part"]}
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen|combining_builder"]
The generated file starts with:
part of 'my_file.dart';
// GENERATED CODE - DO NOT MODIFY BY HAND
And the source file must have:
part 'my_file.g.dart';
PartBuilder#
Like SharedPartBuilder but outputs directly to .g.dart (no combining step). Only use when you're the only generator for a file.
Builder myBuilder(BuilderOptions options) => PartBuilder(
[MyGenerator()],
'.g.dart',
);
LibraryBuilder#
Generates a completely separate library (not a part file). Used for generating standalone files.
Builder myBuilder(BuilderOptions options) => LibraryBuilder(
MyGenerator(),
generatedExtension: '.generated.dart',
);
The output does NOT start with part of — it's a standalone library.
Custom formatOutput#
retrofit uses a custom format function on SharedPartBuilder:
Builder retrofitBuilder(BuilderOptions options) => SharedPartBuilder(
[RetrofitGenerator(options)],
'retrofit',
formatOutput: (code, version) {
final formatted = DartFormatter(languageVersion: version).format(code);
return '// dart format off\n\n$formatted\n// dart format on\n';
},
);
InvalidGenerationSourceError#
Use this to report errors with source location information:
throw InvalidGenerationSourceError(
'@MyAnnotation can only be applied to classes.',
element: element, // Optional: links to source location
todo: 'Remove @MyAnnotation from ${element.displayName}.', // Optional: hint
);
The error message will include the file path and line number of the element.
Complete builder.dart Template#
// 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.
Builder myBuilder(BuilderOptions options) {
// Parse options
final someOption = options.config['some_option'] as String? ?? 'default';
return SharedPartBuilder(
[MyGenerator(someOption: someOption)],
'my_builder',
);
}
How source_gen Fits Into the Build Pipeline#
Source file (model.dart)
↓
[build_runner reads]
↓
LibraryReader created from LibraryElement
↓
Generator.generate(library, buildStep)
↓ [for each annotated element]
GeneratorForAnnotation.generateForAnnotatedElement(element, annotation, buildStep)
↓ [string output]
model.my_builder.g.part (cached)
↓ [source_gen|combining_builder]
model.g.dart (cached or source)
Common Imports Summary#
// All the source_gen essentials
import 'package:source_gen/source_gen.dart';
// Provides: Generator, GeneratorForAnnotation, LibraryReader, ConstantReader,
// TypeChecker, SharedPartBuilder, PartBuilder, LibraryBuilder,
// InvalidGenerationSourceError, AnnotatedElement
import 'package:build/build.dart';
// Provides: Builder, BuildStep, BuilderOptions, AssetId, log
import 'package:analyzer/dart/element/element.dart';
// Provides: Element, LibraryElement, ClassElement, FieldElement, etc.
import 'package:analyzer/dart/element/type.dart';
// Provides: DartType, InterfaceType, TypeParameterType, etc.