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});
}