Published July 26, 2026
Dart Macros Unleashed: Static Metaprogramming Arrives in Dart 3.12
One of the most transformative features to land in the Dart language arrived with Dart 3.12, which shipped alongside Flutter 3.44 at Google I/O 2026: static metaprogramming through macros. After years of experimentation, community feedback, and careful design, Dart macros are now stable, giving developers the ability to generate code at compile time without external build runners, code generation steps, or complex toolchain configuration.
Macros solve a class of problems that has long plagued Dart developers. Tasks like JSON serialization, data class generation, dependency injection wiring, and equality operator overrides previously required either tedious manual implementation or integration with third-party code generators like json_serializable and freezed. These tools worked reasonably well but introduced friction: they required separate build steps, generated files that had to be checked into version control, and sometimes produced hard-to-debug errors when the code generation pipeline failed silently.
How Dart Macros Work
At its core, a Dart macro is a function that runs at compile time and can inspect, modify, or generate Dart code. Macros are declared using the new macro keyword and operate on the AST level, meaning they have full knowledge of the program's structure and types. This is fundamentally different from string-based code generation — macros understand the code they are working with, which enables type-safe transformations and meaningful error messages when something goes wrong.
The macro system introduces two primary kinds of macros: declaration macros and definition macros. Declaration macros augment existing declarations — a class, method, or field — by adding new members or modifying existing ones. Definition macros, on the other hand, can create entirely new declarations from scratch, enabling patterns like automatic repository implementations or data transfer object factories.
Consider a typical use case. Instead of annotating a model class with @JsonSerializable() and running a build runner, Dart 3.12 allows you to define a macro that generates the fromJson and toJson methods directly at compile time. The macro runs as part of the normal compilation pipeline, and the generated code is treated exactly as if you had written it by hand. There are no intermediate files to manage, no separate build step to remember, and no risk of the generated code drifting out of sync with your source annotations.
Ecosystem Impact and Migration
The introduction of stable macros is already reshaping the Dart package ecosystem. Popular packages that previously relied on code generation are releasing macro-based versions that eliminate their build-runner dependency. The json_annotation package was among the first to adopt macros, and early benchmarks show that macro-based JSON serialization is not only simpler to set up but also produces slightly more optimized code because the compiler has full visibility into the generated implementation.
For existing projects, the migration path from build-runner-based code generation to macros is designed to be incremental. Packages can support both approaches simultaneously, allowing teams to migrate module by module. The Dart team has published comprehensive migration guides that cover the most common patterns, and the dart fix command includes automated migrations for straightforward cases like replacing @JsonSerializable() annotations with their macro equivalents.
The Dart analyzer has been updated to provide rich macro-related diagnostics. Developers writing their own macros benefit from step-through debugging support, inline previews of generated code, and detailed error messages when macro expansion fails. The IDE integration — available in VS Code, IntelliJ IDEA, and Android Studio — includes autocomplete for macro-generated members and navigation from macro invocations to the generated implementation.
One important design consideration is that macros are intentionally limited to compile-time execution. They cannot perform I/O, make network requests, or depend on runtime state. This constraint ensures that builds remain deterministic and that macro execution does not introduce security concerns. The compile-time boundary also means that macros work consistently across all Dart targets — mobile, desktop, web, and server — without platform-specific behavior.
The Dart team has indicated that this is just the beginning for static metaprogramming. Future releases are expected to expand the macro API surface, add support for linting macros that enforce project-specific coding conventions, and explore type-level metaprogramming for advanced generic patterns. For now, Dart macros represent a maturity milestone for the language, bringing it on par with languages like Rust, Scala, and Kotlin that have long enjoyed compile-time code generation capabilities. Developers who have been waiting for a cleaner approach to boilerplate reduction in Dart will find that macros deliver exactly that — and more.