LogoDart Code Generation

Defining and Reading Annotations#

Sourced from json_annotation (v4.11.0), dart_mappable (v4.7.0), and retrofit (v4.9.2) runtime libraries.


Defining an Annotation Class#

Annotations are just const constructors. The annotation class lives in a runtime package (regular dependency, not dev_dependency) since it's used in user code.

Minimal annotation#

// package: my_annotations/lib/src/my_annotation.dart

class MyAnnotation {
  const MyAnnotation();
}

Annotation with parameters#

class MyAnnotation {
  final String name;
  final int count;
  final bool enabled;
  final List<String> tags;

  const MyAnnotation({
    this.name = '',
    this.count = 0,
    this.enabled = true,
    this.tags = const [],
  });
}

@Target — Restricting Where Annotations Apply#

Use @Target (from package:meta/meta_meta.dart) to restrict which elements can be annotated:

import 'package:meta/meta_meta.dart';

@Target({TargetKind.classType})   // Only on classes
class MappableClass {
  const MappableClass();
}

@Target({TargetKind.field, TargetKind.getter, TargetKind.parameter})
class MappableField {
  const MappableField();
}

All TargetKind values#

enum TargetKind {
  classType,        // class, abstract class
  enumType,         // enum
  extension,        // extension
  extensionType,    // extension type
  field,            // field (var/final in class body)
  function,         // top-level function
  getter,           // getter
  library,          // library directive
  method,           // method in class
  mixinType,        // mixin
  optionalParameter, // optional parameter ({}) or ([])
  overridableMember, // any overridable class member
  parameter,        // any parameter
  setter,           // setter
  topLevelVariable, // top-level var
  type,             // any type declaration
  typedefType,      // typedef
}

Production examples#

// dart_mappable
@Target({TargetKind.classType})
class MappableClass { ... }

// json_annotation
@Target({TargetKind.classType})
class JsonSerializable { ... }

@Target({TargetKind.field, TargetKind.getter, TargetKind.parameter})
class JsonKey { ... }

// retrofit
// (RestApi has no @Target — Dart still catches misuse at analysis time)
class RestApi { ... }

Annotation Field Types#

Only types that are compile-time constants can be annotation fields:

class MyAnnotation {
  // ✅ Allowed types
  final bool flag;
  final int count;
  final double ratio;
  final String name;
  final List<String> tags;         // const list
  final Map<String, dynamic> config; // const map
  final Type type;                  // a Type literal
  final Object? value;              // any const
  final SomeEnum mode;             // an enum value

  // ❌ NOT allowed (not const)
  // final DateTime date;
  // final Uri uri;
  // final RegExp pattern;

  const MyAnnotation({...});
}

Enums in annotations#

enum CaseStyle { camelCase, snakeCase, pascalCase }

class MyAnnotation {
  final CaseStyle caseStyle;
  const MyAnnotation({this.caseStyle = CaseStyle.camelCase});
}

// Usage:
@MyAnnotation(caseStyle: CaseStyle.snakeCase)
class MyClass { ... }

Nested annotations (const instances as fields)#

class HookAnnotation {
  final MappingHook hook;
  const HookAnnotation({required this.hook});
}

// MappingHook must be const-constructible:
abstract class MappingHook {
  const MappingHook();
}

class UpperCaseHook extends MappingHook {
  const UpperCaseHook();
}

// Usage:
@HookAnnotation(hook: UpperCaseHook())
class MyClass { ... }

Reading Annotations in a Generator#

Via GeneratorForAnnotation (simplest)#

The annotation parameter is already a ConstantReader pointing to the annotation instance:

class MyGenerator extends GeneratorForAnnotation<MyAnnotation> {
  @override
  String generateForAnnotatedElement(
    Element element,
    ConstantReader annotation, // The @MyAnnotation value
    BuildStep buildStep,
  ) {
    // Direct field access
    final name = annotation.read('name').stringValue;          // Throws if null
    final count = annotation.peek('count')?.intValue ?? 0;     // Safe access
    final enabled = annotation.peek('enabled')?.boolValue ?? true;

    // Enum field
    final enumStr = annotation.peek('caseStyle')?.revive().accessor;
    final caseStyle = CaseStyle.values.firstWhere(
      (e) => e.name == enumStr,
      orElse: () => CaseStyle.camelCase,
    );

    // List field
    final tags = annotation.peek('tags')?.listValue
        .map((obj) => obj.toStringValue()!)
        .toList() ?? const <String>[];

    // Type field
    final targetType = annotation.peek('type')?.typeValue;

    return _generate(element as ClassElement, name, count);
  }
}

Via TypeChecker (for reading annotations outside GeneratorForAnnotation)#

const myChecker = TypeChecker.typeNamed(MyAnnotation);

// Check if element has annotation
if (myChecker.hasAnnotationOf(element)) {
  // Get the DartObject
  final DartObject? obj = myChecker.firstAnnotationOf(element);

  // Read fields from DartObject
  final name = obj?.getField('name')?.toStringValue() ?? '';
  final count = obj?.getField('count')?.toIntValue() ?? 0;
  final enabled = obj?.getField('enabled')?.toBoolValue() ?? true;

  // For enum: get variable name
  final enumObj = obj?.getField('caseStyle');
  final enumName = enumObj?.variable?.name; // 'snakeCase', 'camelCase', etc.

  // Wrap in ConstantReader for more convenience
  final reader = ConstantReader(obj);
  final name2 = reader.peek('name')?.stringValue;
}

Reading field-level annotations#

// @MyField annotation on constructor parameters and class fields
for (final field in classElement.fields) {
  final fieldAnnotation = fieldChecker.firstAnnotationOf(field);
  if (fieldAnnotation != null) {
    final key = fieldAnnotation.getField('key')?.toStringValue();
    // Override the JSON key for this field
  }
}

// For constructor parameters
for (final param in constructor.formalParameters) {
  final paramAnnotation = fieldChecker.firstAnnotationOf(param);
  if (paramAnnotation != null) {
    final defaultValue = paramAnnotation.getField('defaultValue');
    // Use the default value
  }
}

DartObject Value Extraction Methods#

Complete reference for DartObject (returned by TypeChecker.firstAnnotationOf and ConstantReader.objectValue):

abstract class DartObject {
  // Type information
  DartType? get type;

  // Value extraction — return null if wrong type or null value
  bool? toBoolValue();
  double? toDoubleValue();
  int? toIntValue();
  String? toStringValue();
  List<DartObject>? toListValue();
  Set<DartObject>? toSetValue();
  Map<DartObject?, DartObject?>? toMapValue();
  DartType? toTypeValue();       // For Type fields
  Symbol? toSymbolValue();

  // Field access (navigate into nested objects)
  DartObject? getField(String name);

  // For enum values and references to static variables
  VariableElement? get variable;
  // variable.name gives the enum value name (e.g. 'snakeCase')

  // Whether this is a special value
  bool get isNull;
}

Pattern: reading a super-class field (dart_mappable does this)#

When annotation fields may be inherited from a superclass annotation, traverse up:

// dart_mappable utils.dart: ObjectReader extension
extension ObjectReader on DartObject {
  DartObject? reads(String field) {
    DartObject? obj = getField(field);
    // Traverse super fields until we find a non-null value
    // (useful for annotations that extend other annotations)
    return obj;
  }
}

Reading Annotations from AST (When Constant Evaluation Fails)#

For complex annotation expressions that can't be constant-evaluated (e.g., function references in @JsonKey(fromJson: myFunc)), you need the AST:

// dart_mappable approach: get the annotation AST node
Future<ArgumentList?> getAnnotationArguments(AstNode? node, Type annotationType) async {
  if (node == null) return null;

  // Find annotation in node's metadata
  late final List<Annotation> metadata;
  if (node is ClassDeclaration) {
    metadata = node.metadata;
  } else if (node is FieldDeclaration) {
    metadata = node.metadata;
  } else {
    return null;
  }

  for (final annotation in metadata) {
    if (annotation.name.name == annotationType.toString().split('.').last) {
      return annotation.arguments;
    }
  }
  return null;
}

// Usage: get AST node for element, then read annotation args
final astNode = await buildStep.resolver.astNodeFor(element.firstFragment, resolve: true);
final args = await getAnnotationArguments(astNode, MyAnnotation);
if (args != null) {
  // Navigate the ArgumentList AST directly
  for (final arg in args.arguments) {
    if (arg is NamedExpression && arg.name.label.name == 'fromJson') {
      // arg.expression is the function reference
    }
  }
}

This is an advanced pattern — only needed when annotations contain non-const values like function references.


Making Annotation Fields Optional with Inheritance#

dart_mappable's pattern for inheriting annotation values from package-level or class-level config:

// 1. Package-level annotation sets defaults
@MappableLib(caseStyle: CaseStyle.snakeCase)
library;

// 2. Class-level annotation can override
@MappableClass(caseStyle: CaseStyle.camelCase)
class MyClass { ... }

// 3. Field-level annotation can further override
class MyClass {
  @MappableField(key: 'custom_name')
  final String myField;
}

In the generator, read at each level and merge:

CaseStyle _resolveFieldCaseStyle(
  FieldElement field,
  ClassElement classElement,
  MappableOptions globalOptions,
) {
  // 1. Field-level (highest priority)
  final fieldAnnotation = fieldChecker.firstAnnotationOf(field);
  if (fieldAnnotation != null) {
    final key = fieldAnnotation.getField('key')?.toStringValue();
    if (key != null) return CaseStyle.none; // explicit key overrides all
  }

  // 2. Class-level
  final classAnnotation = classChecker.firstAnnotationOf(classElement);
  final classCaseStyle = classAnnotation?.getField('caseStyle');
  if (classCaseStyle != null && !classCaseStyle.isNull) {
    return _parseCaseStyle(classCaseStyle);
  }

  // 3. Global options (from build.yaml or @MappableLib)
  return globalOptions.caseStyle ?? CaseStyle.none;
}

Complete Annotation Package Template#

my_annotations/pubspec.yaml#

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

environment:
  sdk: '>=3.0.0 <4.0.0'

# No dependencies needed for pure annotation classes.
# Add meta if you use @Target:
dependencies:
  meta: ^1.9.0

my_annotations/lib/my_annotations.dart#

library my_annotations;

export 'src/my_annotation.dart';
export 'src/my_field_annotation.dart';

my_annotations/lib/src/my_annotation.dart#

import 'package:meta/meta_meta.dart';

/// Apply to classes to generate mapping code.
@Target({TargetKind.classType})
class MyAnnotation {
  /// The JSON key to use for this class's type discriminator.
  final String? discriminatorKey;

  /// The field naming convention.
  final CaseStyle caseStyle;

  /// Whether to include null fields in the output.
  final bool includeNull;

  const MyAnnotation({
    this.discriminatorKey,
    this.caseStyle = CaseStyle.camelCase,
    this.includeNull = true,
  });
}

enum CaseStyle {
  camelCase,
  snakeCase,
  pascalCase,
}

my_annotations/lib/src/my_field_annotation.dart#

import 'package:meta/meta_meta.dart';

/// Apply to fields/parameters to customize serialization.
@Target({TargetKind.field, TargetKind.parameter})
class MyField {
  /// Override the JSON key for this field.
  final String? key;

  /// Skip this field in serialization.
  final bool ignore;

  const MyField({this.key, this.ignore = false});
}