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


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.

DART
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);
}
TEXT 📖 Display only
> **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


3. Annotations

(1) Definition and Usage of Annotations

▶ Example

TEXT 📖 Display only
> **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

DART
// 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'});
}
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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)

DART
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);
}
TEXT 📖 Display only
> **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.
⚠️ Note: dart:mirrors is only available on the Dart VM. It is not supported in Flutter release mode (AOT compilation) or on the Web.


4. dart:mirrors Reflection API

(1) Reflection Capabilities Overview

100%
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]
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
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();
}
TEXT 📖 Display only
> **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)
📌 Key Point: The Dart ecosystem chose "code generation" over "reflection". Mainstream packages like json_serializable, freezed, and dart_mappable are all based on code generation. Flutter's AOT compilation naturally excludes reflection.


6. Annotation-Driven Field Mapping

(1) Bob's Scenario: DataPipeline Field Mapping

▶ Example

TEXT 📖 Display only
> **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)

DART
// 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');
}
TEXT 📖 Display only
> **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

100%
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]
TEXT 📖 Display only
> **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

TEXT 📖 Display only
> **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

DART
// 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());
}
TEXT 📖 Display only
> **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

DART
// ============================================
// 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');
}
TEXT 📖 Display only
> **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:

TEXT 📖 Display only
=== 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 --watch mode 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


📝 Exercises

  1. 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).
  2. 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.
  3. 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.

← Previous Lesson | Next Lesson →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏