Skip to content

Groovy AST Transformations — Compile-Time Metaprogramming

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Groovy AST Transformations. We cover key concepts, practical examples, and best practices to help you master this topic.

Groovy AST transformations modify the abstract syntax tree during compilation, enabling boilerplate reduction and compile-time code generation through annotations like @ToString and @Canonical.

What You'll Learn

  • Built-in AST transformations
  • Using @ToString, @EqualsAndHashCode, @Canonical
  • @Immutable and @AutoClone
  • Writing custom AST transformations

Why It Matters

AST transformations eliminate boilerplate at compile time. Gradle uses AST transformations for task configuration. DodaZIP uses custom transformations to generate serialization code for archive metadata.

Real-World Use

Data class generation (like Lombok for Groovy), compile-time validation, boilerplate reduction in frameworks, and DSL compilation optimization.

flowchart LR
    A["Source Code"] --> B["AST Parser"]
    B --> C["AST Transformations"]
    C --> D["Modified AST"]
    D --> E["Bytecode"]
    C --> F["Local"]
    C --> G["Global"]
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Built-in Transformations

import groovy.transform.*

@ToString
@EqualsAndHashCode
@TupleConstructor
class Person {
    String name
    int age
    String email
}

def p1 = new Person('Alice', 30, 'alice@example.com')
def p2 = new Person('Alice', 30, 'alice@example.com')

println p1            // Person(Alice, 30, alice@example.com)
println p1 == p2      // true

@Canonical

Combines @ToString, @EqualsAndHashCode, and @TupleConstructor:

import groovy.transform.Canonical

@Canonical
class Point {
    int x, y
}

def p = new Point(3, 4)
println p  // Point(3, 4)

@Immutable

Creates an immutable data class with read-only properties:

import groovy.transform.Immutable

@Immutable
class Config {
    String host
    int port
    boolean ssl
}

def cfg = new Config('example.com', 443, true)
// cfg.host = 'other.com'  // ERROR: cannot set immutable property

// Immutable classes are also automatically
// @ToString, @EqualsAndHashCode, @TupleConstructor

@AutoClone

Generates a clone method:

import groovy.transform.AutoClone

@AutoClone
class Document {
    String title
    List<String> tags

    def getDisplay() { "$title [$tags]" }
}

def doc = new Document(title: 'Report', tags: ['urgent', 'draft'])
def clone = doc.clone()
clone.title = 'Copy'
println doc.display     // Report [urgent, draft]
println clone.display   // Copy [urgent, draft]

@Singleton

Implements the singleton pattern:

import groovy.transform.Singleton

@Singleton
class Database {
    def query(String sql) {
        println "Executing: $sql"
    }
}

// Access via instance property
Database.instance.query('SELECT * FROM users')

@PackageScope

Makes generated methods use package-level visibility instead of public:

import groovy.transform.PackageScope
import groovy.transform.ToString

@PackageScope
@ToString
class InternalData {
    String secret
}

def data = new InternalData(secret: 'classified')
println data.toString()  // Can still access in same package

@Log

Adds a logger field:

import groovy.util.logging.Log

@Log
class Service {
    def process() {
        log.info "Processing started"
        log.warning "Low memory"
        log.severe "Critical error"
    }
}

new Service().process()

Custom AST Transformation

import org.codehaus.groovy.transform.*
import org.codehaus.groovy.control.*
import org.codehaus.groovy.ast.*
import org.codehaus.groovy.ast.stmt.*
import org.codehaus.groovy.ast.expr.*

@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
class LogMethodCall implements ASTTransformation {
    void visit(ASTNode[] nodes, SourceUnit source) {
        def classNode = nodes[1]
        classNode.methods.each { method ->
            def oldCode = method.code
            method.code = new BlockStatement([
                new ExpressionStatement(
                    new MethodCallExpression(
                        new PropertyExpression(
                            new ClassExpression(ClassHelper.make(System)),
                            new ConstantStringExpression("out")
                        ),
                        "println",
                        new ArgumentListExpression([
                            new ConstantStringExpression(
                                "Calling ${method.name}"
                            )
                        ])
                    )
                ),
                oldCode
            ] as List<Statement>, new VariableScope())
        }
    }
}

Common Mistakes

1. Forgetting @GroovyASTTransformation annotation

Without it, the transformation is never discovered. The annotation must be present for the compiler to find it.

2. Not registering custom transformations

Custom transformations need a META-INF/services descriptor or compilation classpath registration.

3. Overusing @Canonical

@Canonical generates tuple constructors that change with field order. Use explicit @ToString/@EqualsAndHashCode when stability matters.

4. @Immutable with mutable fields

@Immutable only protects direct field assignment. Fields holding mutable objects (List, Map) are still mutable via their own methods.

5. Transformation ordering issues

Global transformations run before local ones. Relying on results of another transformation can fail due to ordering.

Practice Questions

1. What phase does @ToString transformation run in?

Semantic Analysis phase, after the AST is fully built but before bytecode generation.

2. How does @Canonical differ from @ToString?

@Canonical combines @ToString, @EqualsAndHashCode, and @TupleConstructor in a single annotation.

3. Can you have multiple AST transformations on one class?

Yes. Multiple annotations can be applied and they compose in declaration order.

4. What makes a class @Immutable?

All fields become final, the class is final, and defensive copies are made for collection properties.

Challenge: Write a custom AST transformation that adds a Builder pattern to annotated classes.

FAQ

{{< faq question="Are AST transformations compile-time or runtime?" >}} Compile-time. They execute during compilation and modify the AST before bytecode generation. The resulting bytecode contains no trace of the transformation. {{< /faq >}}

{{< faq question="Which is better — AST transformation or runtime metaprogramming?" >}} AST transformations are faster (no runtime overhead) and type-checkable. Runtime Metaprogramming is more flexible. Use AST transformations for known patterns, MOP for dynamic needs. {{< /faq >}}

{{< faq question="Can AST transformations access annotations at compile time?" >}} Yes. Transformations read annotation parameters from the AST nodes to configure behavior. {{< /faq >}}

{{< faq question="Do AST transformations work with Java classes?" >}} No. AST transformations only apply to Groovy source files during compilation. {{< /faq >}}

{{< faq question="Are there third-party AST transformation libraries?" >}} Yes. Groovy's standard library includes many. External libraries like GroovyExtensions provide additional ones. {{< /faq >}}

Mini Project

Use built-in AST transformations to create a complete REST API model layer.

import groovy.transform.*

@Canonical
@Immutable
class User {
    String id
    String name
    String email
}

@Canonical
@Immutable
class Product {
    String sku
    String name
    BigDecimal price
}

@Singleton
class UserStore {
    private users = [:]

    User find(String id) { users[id] }
    void save(User u) { users[u.id] = u }
}

// Usage
def user = new User(id: '1', name: 'Alice', email: 'alice@test.com')
UserStore.instance.save(user)
println UserStore.instance.find('1')

What's Next

Now that you understand AST transformations, proceed to Groovy's Grape dependency manager.

Topic Description Link
Grape Script dependency management {{< ref "27-grape" >}}
Scripting Script execution {{< ref "25-scripting" >}}
Closures Anonymous functions {{< ref "04-closures" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro