LogoDart Code Generation

Complete Generator Recipes#

Three end-to-end, working examples. Each includes all required files.


Recipe 1: Simple File Transformer#

Transforms .txt files to .dart files. No analyzer needed — just reads/writes files.

lib/src/transformer.dart#

import 'package:build/build.dart';

class TxtToDartBuilder implements Builder {
  const TxtToDartBuilder();

  @override
  Map<String, List<String>> get buildExtensions => const {
    '.txt': ['.txt.dart'],
  };

  @override
  Future<void> build(BuildStep buildStep) async {
    final inputId = buildStep.inputId;
    final outputId = inputId.addExtension('.dart');

    // Read the text file
    final content = await buildStep.readAsString(inputId);

    // Generate a Dart file exposing the content as a string constant
    final varName = inputId.pathSegments.last
        .replaceAll('.txt', '')
        .replaceAll('-', '_')
        .replaceAll(' ', '_');

    final output = '''
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint

/// Content of ${inputId.path}
const String $varName = r\'\'\'
$content
\'\'\';
''';

    await buildStep.writeAsString(outputId, output);
  }
}

lib/builder.dart#

library;

import 'package:build/build.dart';
import 'src/transformer.dart';

Builder txtToDartBuilder(BuilderOptions options) => const TxtToDartBuilder();

build.yaml#

builders:
  txt_to_dart:
    import: "package:my_generator/builder.dart"
    builder_factories: ["txtToDartBuilder"]
    build_extensions: {".txt": [".txt.dart"]}
    auto_apply: dependents
    build_to: source

pubspec.yaml#

name: my_generator
version: 1.0.0
environment:
  sdk: '>=3.0.0 <4.0.0'
dependencies:
  build: ^4.0.0

Recipe 2: Annotation-Driven Generator (String Concatenation)#

Generates fromJson / toJson methods for classes annotated with @Serializable. Uses the json_serializable pattern.

Step 1: Annotation Package#

my_annotations/pubspec.yaml

name: my_annotations
version: 1.0.0
environment:
  sdk: '>=3.0.0 <4.0.0'
dependencies:
  meta: ^1.9.0

my_annotations/lib/src/serializable.dart

import 'package:meta/meta_meta.dart';

/// Apply to classes to generate fromJson/toJson code.
@Target({TargetKind.classType})
class Serializable {
  /// Rename all fields using this strategy.
  final FieldRename fieldRename;

  /// Include null fields in toJson output.
  final bool includeNull;

  const Serializable({
    this.fieldRename = FieldRename.none,
    this.includeNull = true,
  });
}

/// Apply to fields to override serialization behavior.
@Target({TargetKind.field, TargetKind.parameter})
class SerializableField {
  /// Override the JSON key for this field.
  final String? name;

  /// Exclude this field from serialization.
  final bool ignore;

  const SerializableField({this.name, this.ignore = false});
}

enum FieldRename { none, snake, camel, pascal }

my_annotations/lib/my_annotations.dart

library my_annotations;

export 'src/serializable.dart';

Step 2: Generator Package#

my_generator/pubspec.yaml

name: my_generator
version: 1.0.0
environment:
  sdk: '>=3.0.0 <4.0.0'
dependencies:
  build: ^4.0.0
  analyzer: '>=8.0.0 <11.0.0'
  source_gen: ^4.0.0
  dart_style: ^3.0.0
  my_annotations: ^1.0.0
dev_dependencies:
  build_runner: ^2.10.0
  build_test: ^3.3.0
  test: ^1.25.0

my_generator/build.yaml

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

my_generator/lib/src/generator.dart

import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:build/build.dart';
import 'package:my_annotations/my_annotations.dart';
import 'package:source_gen/source_gen.dart';

const _fieldChecker = TypeChecker.typeNamed(SerializableField);

class SerializableGenerator extends GeneratorForAnnotation<Serializable> {
  const SerializableGenerator();

  @override
  Iterable<String> generateForAnnotatedElement(
    Element element,
    ConstantReader annotation,
    BuildStep buildStep,
  ) sync* {
    if (element is! ClassElement) {
      throw InvalidGenerationSourceError(
        '@Serializable can only be applied to classes.',
        element: element,
      );
    }

    final fieldRenameStr = annotation.peek('fieldRename')?.revive().accessor;
    final fieldRename = FieldRename.values.firstWhere(
      (e) => e.name == fieldRenameStr,
      orElse: () => FieldRename.none,
    );
    final includeNull = annotation.peek('includeNull')?.boolValue ?? true;

    yield _generateFromJson(element);
    yield _generateToJson(element, fieldRename, includeNull);
  }

  String _jsonKey(FieldElement field, FieldRename rename) {
    // 1. Check for @SerializableField(name: ...)
    final fieldAnnotation = _fieldChecker.firstAnnotationOf(field);
    if (fieldAnnotation != null) {
      final customName = fieldAnnotation.getField('name')?.toStringValue();
      if (customName != null) return customName;
    }

    // 2. Apply rename strategy
    final name = field.name!;
    return switch (rename) {
      FieldRename.snake => _toSnakeCase(name),
      FieldRename.camel => name, // already camelCase
      FieldRename.pascal => '${name[0].toUpperCase()}${name.substring(1)}',
      FieldRename.none => name,
    };
  }

  bool _shouldIgnore(FieldElement field) {
    final annotation = _fieldChecker.firstAnnotationOf(field);
    return annotation?.getField('ignore')?.toBoolValue() ?? false;
  }

  List<FieldElement> _getFields(ClassElement element) {
    return element.fields
        .where((f) => !f.isStatic && !f.isSynthetic && f.isPublic)
        .where((f) => !_shouldIgnore(f))
        .toList();
  }

  String _generateFromJson(ClassElement element) {
    final className = element.name;
    final fields = _getFields(element);

    final args = fields.map((field) {
      final key = _jsonKey(field, FieldRename.none);
      final type = field.type;
      final isNullable = type.nullabilitySuffix == NullabilitySuffix.question;
      final typeName = type.getDisplayString(withNullability: false);

      // Basic type deserialization
      final jsonAccess = "json['$key']";
      final deser = _deserialize(type, jsonAccess);

      return '${field.name}: $deser,';
    }).join('\n    ');

    return '''
$className _\$${className}FromJson(Map<String, dynamic> json) => $className(
    $args
  );''';
  }

  String _generateToJson(
    ClassElement element,
    FieldRename fieldRename,
    bool includeNull,
  ) {
    final className = element.name;
    final fields = _getFields(element);

    final entries = fields.map((field) {
      final key = _jsonKey(field, fieldRename);
      final isNullable = field.type.nullabilitySuffix == NullabilitySuffix.question;
      final value = _serialize(field.type, 'instance.${field.name}');

      if (!includeNull && isNullable) {
        return "if (instance.${field.name} != null) '$key': $value,";
      }
      return "'$key': $value,";
    }).join('\n    ');

    return '''
Map<String, dynamic> _\$${className}ToJson($className instance) => <String, dynamic>{
    $entries
  };''';
  }

  String _deserialize(DartType type, String expression) {
    final baseType = type.promoteToNonNull;
    final typeName = baseType.getDisplayString(withNullability: false);
    final isNullable = type.nullabilitySuffix == NullabilitySuffix.question;
    final nullCheck = isNullable ? '?' : '';

    if (baseType is InterfaceType) {
      if (baseType.isDartCoreString) return '$expression as String$nullCheck';
      if (baseType.isDartCoreInt) return '($expression as num$nullCheck)$nullCheck.toInt()';
      if (baseType.isDartCoreDouble) return '($expression as num$nullCheck)$nullCheck.toDouble()';
      if (baseType.isDartCoreBool) return '$expression as bool$nullCheck';
      if (baseType.isDartCoreList) {
        final inner = baseType.typeArguments.firstOrNull;
        if (inner != null) {
          return '($expression as List<dynamic>$nullCheck)$nullCheck'
              '.map((e) => ${_deserialize(inner, 'e')}).toList()';
        }
        return '$expression as List<dynamic>$nullCheck';
      }
      if (baseType.isDartCoreMap) {
        return '($expression as Map<String, dynamic>$nullCheck)';
      }
      // Assume it has fromJson
      if (isNullable) {
        return '$expression == null ? null : $typeName.fromJson($expression as Map<String, dynamic>)';
      }
      return '$typeName.fromJson($expression as Map<String, dynamic>)';
    }

    return '$expression as $typeName$nullCheck';
  }

  String _serialize(DartType type, String expression) {
    final baseType = type.promoteToNonNull;
    final isNullable = type.nullabilitySuffix == NullabilitySuffix.question;

    if (baseType is InterfaceType) {
      if (baseType.isDartCoreString ||
          baseType.isDartCoreInt ||
          baseType.isDartCoreDouble ||
          baseType.isDartCoreBool) {
        return expression;
      }
      if (baseType.isDartCoreList) {
        final inner = baseType.typeArguments.firstOrNull;
        if (inner != null && !_isPrimitive(inner)) {
          final q = isNullable ? '?' : '';
          return '$expression$q.map((e) => ${_serialize(inner, 'e')}).toList()';
        }
        return expression;
      }
      if (baseType.isDartCoreMap) return expression;

      // Assume it has toJson
      final q = isNullable ? '?' : '';
      return '$expression$q.toJson()';
    }

    return expression;
  }

  bool _isPrimitive(DartType type) {
    if (type is! InterfaceType) return false;
    return type.isDartCoreString ||
        type.isDartCoreInt ||
        type.isDartCoreDouble ||
        type.isDartCoreBool;
  }

  String _toSnakeCase(String camelCase) {
    return camelCase.replaceAllMapped(
      RegExp(r'[A-Z]'),
      (m) => '_${m.group(0)!.toLowerCase()}',
    );
  }
}

my_generator/lib/builder.dart

library;

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

Builder serializableBuilder(BuilderOptions options) =>
    SharedPartBuilder([const SerializableGenerator()], 'serializable');

Step 3: User Code#

User's pubspec.yaml

dependencies:
  my_annotations: ^1.0.0
dev_dependencies:
  my_generator: ^1.0.0
  build_runner: ^2.10.0

User's source file

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

part 'user.g.dart';

@Serializable(fieldRename: FieldRename.snake, includeNull: false)
class User {
  final String id;
  final String firstName;
  final String? email;

  @SerializableField(name: 'family_name')
  final String lastName;

  @SerializableField(ignore: true)
  final String internalSecret;

  const User({
    required this.id,
    required this.firstName,
    required this.lastName,
    this.email,
    required this.internalSecret,
  });

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

Generated output (lib/user.g.dart)

// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint

part of 'user.dart';

User _$UserFromJson(Map<String, dynamic> json) => User(
    id: json['id'] as String,
    firstName: json['first_name'] as String,
    lastName: json['family_name'] as String,
    email: json['email'] as String?,
    internalSecret: json['internal_secret'] as String,
  );

Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
    'id': instance.id,
    'first_name': instance.firstName,
    'family_name': instance.lastName,
    if (instance.email != null) 'email': instance.email,
  };

Recipe 3: code_builder Approach (Retrofit Pattern)#

Generates a concrete implementation class for an abstract API interface. Uses code_builder for type-safe code AST.

Annotation#

@Target({TargetKind.classType})
class ApiClient {
  final String baseUrl;
  const ApiClient({this.baseUrl = ''});
}

@Target({TargetKind.method})
class GET {
  final String path;
  const GET(this.path);
}

Generator#

import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';
import 'package:source_gen/source_gen.dart';

const _getChecker = TypeChecker.typeNamed(GET);

class ApiClientGenerator extends GeneratorForAnnotation<ApiClient> {
  @override
  String generateForAnnotatedElement(
    Element element,
    ConstantReader annotation,
    BuildStep buildStep,
  ) {
    if (element is! ClassElement) {
      throw InvalidGenerationSourceError(
        '@ApiClient can only be applied to abstract classes.',
        element: element,
      );
    }

    final baseUrl = annotation.peek('baseUrl')?.stringValue ?? '';
    final className = '_\$${element.name}';

    final implClass = Class((c) {
      c
        ..name = className
        ..extend = refer(element.name)
        ..fields.addAll([
          Field((f) => f
            ..name = '_baseUrl'
            ..type = refer('String')
            ..modifier = FieldModifier.final$),
          Field((f) => f
            ..name = '_client'
            ..type = refer('http.Client')
            ..modifier = FieldModifier.final$),
        ])
        ..constructors.add(Constructor((con) {
          con
            ..requiredParameters.addAll([
              Parameter((p) => p
                ..name = '_client'
                ..toThis = true),
            ])
            ..optionalParameters.add(
              Parameter((p) => p
                ..name = 'baseUrl'
                ..named = true
                ..type = refer('String')
                ..defaultTo = literalString(baseUrl).code),
            )
            ..initializers.add(Code("_baseUrl = baseUrl"));
        }))
        ..methods.addAll(_generateMethods(element));
    });

    final emitter = DartEmitter(useNullSafetySyntax: true);
    return DartFormatter(
      languageVersion: DartFormatter.latestLanguageVersion,
    ).format(implClass.accept(emitter).toString());
  }

  Iterable<Method> _generateMethods(ClassElement element) {
    return element.methods
        .where((m) => m.isAbstract)
        .where((m) => _getChecker.hasAnnotationOf(m))
        .map(_generateMethod);
  }

  Method _generateMethod(MethodElement method) {
    final getAnnotation = _getChecker.firstAnnotationOf(method)!;
    final path = getAnnotation.getField('path')?.toStringValue() ?? '/';

    // Detect return type (must be Future<Something>)
    final returnType = method.returnType;
    final returnTypeName = returnType.getDisplayString(withNullability: true);

    return Method((m) {
      m
        ..name = method.name
        ..returns = refer(returnTypeName)
        ..modifier = MethodModifier.async
        ..annotations.add(refer('override'))
        ..requiredParameters.addAll(
          method.formalParameters
              .where((p) => p.isRequiredPositional)
              .map((p) => Parameter((param) {
                    param
                      ..name = p.name!
                      ..type = refer(
                        p.type.getDisplayString(withNullability: true),
                      );
                  })),
        )
        ..body = Block.of([
          // final uri = Uri.parse('$_baseUrl$path');
          declareFinal('uri')
              .assign(
                refer('Uri').property('parse').call([
                  literalString('\$_baseUrl$path'),
                ]),
              )
              .statement,

          // final response = await _client.get(uri);
          declareFinal('response')
              .assign(
                refer('_client')
                    .property('get')
                    .call([refer('uri')])
                    .awaited,
              )
              .statement,

          // return SomeType.fromJson(jsonDecode(response.body));
          const Code('return SomeType.fromJson(jsonDecode(response.body));'),
        ]);
    });
  }
}

lib/builder.dart#

library;

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

Builder apiClientBuilder(BuilderOptions options) =>
    SharedPartBuilder([ApiClientGenerator()], 'api_client');

build.yaml#

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

Recipe 4: Package-Level Generator (Runs Once Per Package)#

Generates a single file aggregating all annotated classes in a package.

class AggregatorBuilder implements Builder {
  @override
  Map<String, List<String>> get buildExtensions => const {
    r'$package$': ['lib/generated/all_models.dart'],
  };

  @override
  Future<void> build(BuildStep buildStep) async {
    final outputId = AssetId(
      buildStep.inputId.package,
      'lib/generated/all_models.dart',
    );

    // Find all dart files in lib/
    final modelClasses = <ClassElement>[];
    await for (final assetId in buildStep.findAssets(Glob('lib/**.dart'))) {
      // Only process non-generated files
      if (assetId.path.endsWith('.g.dart')) continue;
      if (!await buildStep.resolver.isLibrary(assetId)) continue;

      final library = await buildStep.resolver.libraryFor(assetId);
      for (final element in library.topLevelElements.whereType<ClassElement>()) {
        if (myChecker.hasAnnotationOf(element)) {
          modelClasses.add(element);
        }
      }
    }

    // Generate the aggregator file
    final buffer = StringBuffer();
    buffer.writeln('// GENERATED CODE - DO NOT MODIFY BY HAND');
    buffer.writeln('');
    buffer.writeln("import 'package:${buildStep.inputId.package}/models.dart';");
    buffer.writeln('');
    buffer.writeln('const allModelNames = [');
    for (final cls in modelClasses) {
      buffer.writeln("  '${cls.name}',");
    }
    buffer.writeln('];');

    await buildStep.writeAsString(outputId, buffer.toString());
  }
}
# build.yaml
builders:
  aggregator:
    import: "package:my_generator/builder.dart"
    builder_factories: ["aggregatorBuilder"]
    build_extensions: {r"$package$": ["lib/generated/all_models.dart"]}
    auto_apply: dependents
    build_to: source
    is_optional: false