Dart: Dart Metaprogramming and Reflection — Annotations Code
Last updated: 2026-08-26
Metaprogramming is writing code that writes code — automating repetitive labor so developers can focus on business logic.
1. What You Will Learn
- dart:mirrors reflection API (VM only limitation)
- Annotations: Definition and reading
- Code generation vs. reflection trade-offs
- reflectionless design philosophy and Flutter's choice
- Bob's scenario: Annotation-driven field mapping in DataPipeline
2. A Developer's True Story
(1) Pain Point: Manual Serialization Code Accounts for 60% of Development Effort
Bob's DataPipeline has 20 data model classes, each requiring fromJson/toJson methods. Manually writing serialization code for 20 classes took 3 days and was error-prone — a single misspelled field name caused the parsing failure of 100,000 records.
(2) The Code Generation Solution
Dart chose code generation over reflection. By annotating model classes, build_runner automatically generates serialization code.
import 'package:json_annotation/json_annotation.dart';
part 'order.g.dart';
@JsonSerializable()
class Order {
final String id;
final double amount;
Order({required this.id, required this.amount});
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
Map<String, dynamic> toJson() => _$OrderToJson(this);
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; execution results may vary slightly depending on the SDK version.
(3) Benefits
- Serialization code for 20 classes generated automatically, from 3 days to 30 minutes
- Misspelled field names are caught at compile-time
- AOT-compilation friendly, supporting Flutter/Web on all platforms
3. Annotations
(1) Definition and Usage of Annotations
▶ Example
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; execution results may vary slightly depending on the SDK version.
: Custom Annotations
// Define a custom annotation
class Column {
final String name;
final bool nullable;
final String? defaultValue;
const Column({
required this.name,
this.nullable = false,
this.defaultValue,
});
}
class Table {
final String name;
const Table(this.name);
}
// Apply annotations to a class
@Table('orders')
class Order {
@Column(name: 'order_id')
final String id;
@Column(name: 'amount', nullable: false)
final double amount;
@Column(name: 'status', defaultValue: 'pending')
final String status;
Order({required this.id, required this.amount, this.status = 'pending'});
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; execution results may vary slightly depending on the SDK version.
▶ Example
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; execution results may vary slightly depending on the SDK version.
: Reading Annotations Using Reflection (VM only)
import 'dart:mirrors';
// Read annotations using reflection (VM only!)
void printTableInfo(Type type) {
final classMirror = reflectClass(type);
// Read class-level annotations
for (final metadata in classMirror.metadata) {
if (metadata.reflectee is Table) {
final table = metadata.reflectee as Table;
print('Table: ${table.name}');
}
}
// Read field-level annotations
classMirror.declarations.forEach((key, declaration) {
if (declaration is VariableMirror) {
for (final metadata in declaration.metadata) {
if (metadata.reflectee is Column) {
final column = metadata.reflectee as Column;
print(' ${declaration.simpleName}: ${column.name} '
'(nullable: ${column.nullable}, default: ${column.defaultValue})');
}
}
}
});
}
void main() {
printTableInfo(Order);
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; execution results may vary slightly depending on the SDK version.
4. dart:mirrors Reflection API
(1) Reflection Capabilities Overview
graph TD A[Metaprogramming] --> B[dart:mirrors Reflection] A --> C[Annotations + Code Generation] B --> B1[Runtime flexible] B --> B2[VM only / AOT unavailable] C --> C1[Compile-time generation] C --> C2[AOT compatible / Flutter friendly]
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; execution results may vary slightly depending on the SDK version.
▶ Example
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; execution results may vary slightly depending on the SDK version.
: Reflection API Usage
import 'dart:mirrors';
class Product {
final String name;
final double price;
String category;
Product({required this.name, required this.price, this.category = 'General'});
String formatPrice() => '\$${price.toStringAsFixed(2)} USD';
double applyDiscount(double rate) => price * (1 - rate);
}
void reflectOnProduct() {
final mirror = reflectClass(Product);
// List all instance methods
print('Methods:');
mirror.instanceMembers.forEach((name, member) {
if (member is MethodMirror && !member.isConstructor && !member.isStatic) {
print(' $name: ${member.returnType.reflectedType}');
}
});
// Create instance via reflection
final instance = mirror.newInstance(
Symbol(''),
[],
{#name: 'Laptop', #price: 1299.99, #category: 'Electronics'},
);
// Invoke method via reflection
final formatted = instance.invoke(#formatPrice, []);
print('Formatted: ${formatted.reflectee}');
final discounted = instance.invoke(#applyDiscount, [0.1]);
print('Discounted: \$${discounted.reflectee.toStringAsFixed(2)} USD');
}
void main() {
reflectOnProduct();
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; execution results may vary slightly depending on the SDK version.
| Reflection Capability | API | Description |
|---|---|---|
| Get class information | reflectClass(Type) |
Class name, methods, fields |
| Create instance | newInstance() |
Dynamic instantiation |
| Invoke method | invoke() |
Dynamic method invocation |
| Read field | getField() |
Dynamic property access |
| Read annotations | .metadata |
Get metadata |
5. Code Generation vs. Reflection
(1) Comparison and Trade-offs
| Dimension | dart:mirrors Reflection | Code Generation (build_runner) |
|---|---|---|
| Runtime | Flexible, runtime decisions | Determined at compile-time |
| AOT compatible | Incompatible | Compatible |
| Flutter support | Unavailable in release mode | Supported in all modes |
| Web support | Unavailable | Supported |
| Performance | Runtime overhead | Zero runtime overhead |
| Developer experience | No generation step needed | Requires build_runner step |
| Debugging | Difficult (dynamic dispatch) | Simple (generated code is readable) |
6. Annotation-Driven Field Mapping
(1) Bob's Scenario: DataPipeline Field Mapping
▶ Example
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; execution results may vary slightly depending on the SDK version.
: Manual Annotation Processor (Simulating Code Generation)
// Custom annotations for field mapping
class FieldMapping {
final String csvColumn;
final String? defaultValue;
final bool required;
const FieldMapping({
required this.csvColumn,
this.defaultValue,
this.required = true,
});
}
class ModelMapping {
final String tableName;
const ModelMapping(this.tableName);
}
// Model with field mapping annotations
@ModelMapping('customers')
class Customer {
@FieldMapping(csvColumn: 'customer_id')
final String id;
@FieldMapping(csvColumn: 'customer_name', defaultValue: 'Unknown')
final String name;
@FieldMapping(csvColumn: 'email', required: false)
final String? email;
@FieldMapping(csvColumn: 'total_spent', defaultValue: '0')
final double totalSpent;
Customer({
required this.id,
required this.name,
this.email,
this.totalSpent = 0,
});
// Manual mapping (in real project, this would be generated)
static Customer fromCsvMap(Map<String, String> csvRow) {
return Customer(
id: csvRow['customer_id'] ?? '',
name: csvRow['customer_name'] ?? 'Unknown',
email: csvRow['email'],
totalSpent: double.tryParse(csvRow['total_spent'] ?? '0') ?? 0,
);
}
}
void main() {
final csvRow = {
'customer_id': 'CUST-001',
'customer_name': 'Alice',
'email': 'alice@example.com',
'total_spent': '52500.75',
};
final customer = Customer.fromCsvMap(csvRow);
print('Customer: ${customer.id}, ${customer.name}');
print('Email: ${customer.email ?? "N/A"}');
print('Total: \$${customer.totalSpent.toStringAsFixed(2)} USD');
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; execution results may vary slightly depending on the SDK version.
7. Reflectionless Design Philosophy
(1) Why Dart Chose No Reflection
graph TD A[Reflection vs Code Generation] --> B[Reflection] A --> C[Code Generation] B --> B1[Runtime flexibility] B --> B2[AOT incompatible] B --> B3[Flutter/Web blocked] B --> B4[Performance overhead] C --> C1[Compile-time certainty] C --> C2[AOT compatible] C --> C3[Flutter/Web friendly] C --> C4[Zero runtime cost]
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; execution results may vary slightly depending on the SDK version.
| Design Principle | Description |
|---|---|
| AOT first | Flutter release uses AOT compilation; reflection is unavailable |
| Tree shaking | Compiler removes unused code; reflection prevents tree shaking |
| Performance first | Reflection has runtime overhead; code generation has zero overhead |
| Type safety | Code generation maintains type safety; reflection loses type checking |
▶ Example
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; execution results may vary slightly depending on the SDK version.
: Code Generation as an Alternative to Reflection
// Instead of reflection-based JSON parsing:
// dynamic parseJson(Map<String, dynamic> json, Type type) { ... } // BAD
// Use code generation-based approach:
// 1. Define model with annotation
// @JsonSerializable()
// class Order { ... }
// 2. Run: dart run build_runner build
// 3. Generated code provides type-safe parsing:
// factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
// Manual version (simulating generated code)
class Order {
final String id;
final double amount;
Order({required this.id, required this.amount});
// "Generated" fromJson
factory Order.fromJson(Map<String, dynamic> json) => Order(
id: json['id'] as String,
amount: (json['amount'] as num).toDouble(),
);
// "Generated" toJson
Map<String, dynamic> toJson() => {
'id': id,
'amount': amount,
};
}
void main() {
final json = {'id': 'ORD-001', 'amount': 1500.0};
final order = Order.fromJson(json); // Type-safe!
print(order.toJson());
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; execution results may vary slightly depending on the SDK version.
8. Complete Example: DataPipeline Annotation-Driven Mapping
// ============================================
// DataPipeline Annotation-Driven Mapping
// Simulates code generation for CSV-to-Model
// ============================================
// Annotations
class CsvField {
final String column;
final String? defaultValue;
final bool required;
const CsvField({
required this.column,
this.defaultValue,
this.required = true,
});
}
class CsvModel {
final String fileName;
const CsvModel(this.fileName);
}
// Model classes
@CsvModel('orders')
class Order {
@CsvField(column: 'order_id')
final String id;
@CsvField(column: 'total_amount', defaultValue: '0')
final double amount;
@CsvField(column: 'order_status', defaultValue: 'pending')
final String status;
@CsvField(column: 'category', required: false)
final String? category;
Order({
required this.id,
required this.amount,
this.status = 'pending',
this.category,
});
// In real project, this would be generated by build_runner
static Order fromCsvRow(Map<String, String> row) => Order(
id: row['order_id'] ?? '',
amount: double.tryParse(row['total_amount'] ?? '0') ?? 0,
status: row['order_status'] ?? 'pending',
category: row['category'],
);
double get tax => amount * 0.08;
double get total => amount + tax;
@override
String toString() => 'Order($id, \$${amount.toStringAsFixed(2)}, $status${category != null ? ", $category" : ""})';
}
// Mapper (simulating generated code)
class CsvMapper``<T>`` {
final T Function(Map<String, String>) fromCsvRow;
CsvMapper(this.fromCsvRow);
List``<T>`` mapAll(List<Map<String, String>> rows) =>
rows.map(fromCsvRow).toList();
(List``<T>`` valid, List<(int, String)> errors) mapSafe(
List<Map<String, String>> rows) {
final valid = ``<T>``[];
final errors = <(int, String)>[];
for (var i = 0; i < rows.length; i++) {
try {
valid.add(fromCsvRow(rows[i]));
} catch (e) {
errors.add((i + 1, e.toString()));
}
}
return (valid, errors);
}
}
void main() {
// Simulated CSV data (already parsed into maps)
final csvRows = <Map<String, String>>[
{'order_id': 'ORD-001', 'total_amount': '1500.00', 'order_status': 'completed', 'category': 'Electronics'},
{'order_id': 'ORD-002', 'total_amount': '3200.50', 'order_status': 'completed', 'category': 'Electronics'},
{'order_id': 'ORD-003', 'total_amount': '890.00', 'order_status': 'pending', 'category': 'Clothing'},
{'order_id': 'ORD-004', 'total_amount': '50.00', 'order_status': 'completed'},
];
// Map to model objects
final mapper = CsvMapper``<Order>``(Order.fromCsvRow);
final (valid, errors) = mapper.mapSafe(csvRows);
print('=== DataPipeline CSV Import Report ===');
print('Rows: ${csvRows.length}');
print('Valid: ${valid.length}');
print('Errors: ${errors.length}');
if (errors.isNotEmpty) {
print('\nErrors:');
for (final (line, msg) in errors) {
print(' Line $line: $msg');
}
}
// Process valid orders
double totalRevenue = 0;
for (final order in valid) {
if (order.status == 'completed') {
totalRevenue += order.total;
}
print(' $order');
}
print('\nRevenue (completed): \$${totalRevenue.toStringAsFixed(2)} USD');
}
> **Output:** Run in local DartPad or using `dart run`. All examples in the Dart course are based on Dart 3.x / Flutter 3.x; execution results may vary slightly depending on the SDK version.
Output:
=== DataPipeline CSV Import Report ===
Rows: 4
Valid: 4
Errors: 0
Order(ORD-001, $1500.00, completed, Electronics)
Order(ORD-002, $3200.50, completed, Electronics)
Order(ORD-003, $890.00, pending, Clothing)
Order(ORD-004, $50.00, completed)
Revenue (completed): $5132.54 USD
❓ FAQ
Q: Why doesn't Flutter support dart:mirrors? A: Flutter uses AOT compilation to native code. AOT requires all type information to be determined at compile-time. Reflection dynamically looks up types at runtime, which conflicts with AOT's tree shaking and compilation optimizations.
Q: What are annotations themselves useful for? A: Annotations themselves do not execute any logic; they are just metadata. They need to be read by reflection (VM) or processed by a code generator (build_runner) to be effective.
Q: Do I need to re-run build_runner every time I change the code? A: Yes, but there is a
--watchmode that monitors file changes and automatically regenerates. Use watch mode during development and build mode before release.
Q: What's the difference between json_serializable and manually writing fromJson? A: json_serializable automatically generates code, avoiding manual writing errors, and supports nested objects and custom conversions. Manual writing is simple but error-prone, and nested objects become even more painful.
Q: Will Dart support macros in the future? A: The Dart team is developing a macro system (macro package), aiming to replace some of build_runner's functionality for a better developer experience. However, it is not yet stable.
Q: Does code generation increase package size? A: Yes, because the generated code is included in the compilation output. However, reflection also increases size (by preventing tree shaking); the difference is minimal.
Q: How do I debug generated code? A: You can directly open and read the generated .g.dart files for debugging. The code generated by build_runner is standard Dart code; you can set breakpoints.
📖 Summary
- dart:mirrors provides runtime reflection capabilities but is limited to the VM environment; it is unavailable for Flutter AOT and Web
- Annotations are metadata markers that require reflection or a code generator to be consumed
- The Dart ecosystem chose "code generation" over "reflection" for compatibility with AOT/Flutter/Web
- The reflectionless design philosophy: determinism at compile-time, zero runtime overhead, preserving tree shaking
- DataPipeline uses @CsvField annotations to mark field mapping, simulating code generation
📝 Exercises
- Basic (Difficulty ⭐): Define 3 custom annotations (@ApiEndpoint, @Required, @DefaultValue) and apply them to a class. Use dart:mirrors to read the annotation information (note: can only run on VM).
- Intermediate (Difficulty ⭐⭐): Create a json_serializable project, generate fromJson/toJson code for an Order class with 5 fields. View the generated .g.dart file and understand the generation logic.
- Challenge (Difficulty ⭐⭐⭐): Design a simple code generator: read class definitions annotated with @CsvField and generate a static fromCsvRow method. Hint: you can use the source_gen package or simple string templates.