LogoDart Code Generation

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 elements
  • ConstantReader — safe access to annotation constant values
  • TypeChecker — 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 output
  • Iterable<String> — multiple code pieces (joined with \n\n)
  • Future<String> — async generation
  • null — 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 LibraryElementGenerator.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.