ZetCode

Groovy AST Transformations

last modified October 15, 2026

An AST transformation rewrites the abstract syntax tree of a class while it is being compiled. Groovy ships with a rich set of annotations that trigger these transformations, so you get generated methods, immutable classes, and builders without writing boilerplate. Unlike the runtime tricks of metaprogramming, the work happens at compile time, so the generated code is visible to the compiler and the IDE. This tutorial walks through the most useful built-in annotations.

@ToString

Adding @ToString to a class generates a toString method that lists the class name, its identity hash, and the value of every property.

ToStringDemo.groovy
import groovy.transform.ToString

@ToString
class User {
    String name
    int age
}

def user = new User(name: "Alice", age: 30)
println user

Printing the object shows something like User(Alice, 30). The includeNames = true option adds the property names, and includeFields or excludes control which members appear.

$ groovy ToStringDemo.groovy
User(Alice, 30)

@EqualsAndHashCode

@EqualsAndHashCode generates equals and hashCode based on the class properties. Two instances with the same values then compare as equal and can be used safely as map keys.

EqualsHashCodeDemo.groovy
import groovy.transform.EqualsAndHashCode

@EqualsAndHashCode
class Point {
    int x
    int y
}

def a = new Point(x: 1, y: 2)
def b = new Point(x: 1, y: 2)

println a == b
println(([a] as Set).contains(b))

Both a == b and the set membership test return true because the generated methods compare the coordinates. The excludes attribute lets you ignore fields that should not participate in equality.

$ groovy EqualsHashCodeDemo.groovy
true
true

@Canonical

@Canonical combines @ToString, @EqualsAndHashCode, and a tuple constructor in one annotation. It is the quickest way to make a well-behaved value class.

CanonicalDemo.groovy
import groovy.transform.Canonical

@Canonical
class Product {
    String name
    BigDecimal price
}

def p1 = new Product("Widget", 9.99)
def p2 = new Product("Widget", 9.99)

println p1
println p1 == p2

The tuple constructor allows positional arguments, toString produces a readable string, and equality works by value. For most data holders, @Canonical replaces dozens of lines of hand-written code.

$ groovy CanonicalDemo.groovy
Product(Widget, 9.99)
true

@Immutable

@Immutable makes a class final with final properties, generates read-only accessors, and produces an immutable copy on construction. Attempts to modify an instance after creation fail fast.

ImmutableDemo.groovy
import groovy.transform.Immutable

@Immutable
class Money {
    String currency
    BigDecimal amount
}

def price = new Money("USD", 19.99)
println price

def discounted = new Money(currency: "USD", amount: 14.99)
println discounted.amount

The price instance cannot be changed after construction, which makes it safe to share across threads. Immutable values are natural map keys and lead to fewer aliasing bugs.

$ groovy ImmutableDemo.groovy
Money(USD, 19.99)
14.99

@Delegate

@Delegate forwards any method it does not recognize to the annotated field. This implements composition without writing delegation methods by hand.

DelegateDemo.groovy
import groovy.lang.Delegate

class Printer {
    void print(String text) {
        println "Printing: $text"
    }
}

class Report {
    @Delegate Printer printer = new Printer()

    String title
}

def report = new Report(title: "Monthly")
report.print(report.title)

Even though Report declares no print method, the call is routed to the delegate. This keeps a clear "has-a" relationship while exposing the composed object's interface.

$ groovy DelegateDemo.groovy
Printing: Monthly

@Sortable

@Sortable generates compareTo that compares properties in declaration order. The class becomes Comparable, so lists of instances sort with a single call.

SortableDemo.groovy
import groovy.transform.Sortable

@Sortable
class Employee {
    String department
    String name
}

def staff = [
    new Employee(department: "Sales", name: "Bob"),
    new Employee(department: "Engineering", name: "Alice"),
    new Employee(department: "Engineering", name: "Carol")
]

staff.sort().each { println "$it.department / $it.name" }

Because department is declared first, employees sort by department and then by name. The includes and excludes options customize which properties take part.

$ groovy SortableDemo.groovy
Engineering / Alice
Engineering / Carol
Sales / Bob

@Builder

@Builder generates a fluent builder for a class, so objects can be constructed through named calls instead of positional constructors.

BuilderAnnotation.groovy
import groovy.transform.builder.Builder

@Builder
class Server {
    String host
    int port
    boolean secure
}

def server = Server.builder()
    .host("example.com")
    .port(8443)
    .secure(true)
    .build()

println server.host
println server.secure

The generated builder exposes one method per property and a terminal build(). This is especially welcome for classes with many optional fields, where a long constructor becomes unwieldy.

$ groovy BuilderAnnotation.groovy
example.com
true

@Grab

@Grab downloads a dependency from a Maven repository at compile time and adds it to the classpath. It is part of Grape and is ideal for single file scripts that need a third-party library.

GrabDemo.groovy
@Grab('org.apache.commons:commons-lang3:3.14.0')
import org.apache.commons.lang3.StringUtils

println StringUtils.capitalize("groovy")

Running this script resolves Commons Lang into the local cache and prints Groovy. The coordinates are group:artifact:version, and a @GrabResolver annotation can point at a custom repository.

$ groovy GrabDemo.groovy
Groovy

@TailRecursive

@TailRecursive rewrites tail-recursive methods into loops, avoiding stack overflow for deep recursion. The recursive call must be the final statement of the method.

TailRecursiveDemo.groovy
import groovy.transform.TailRecursive

@TailRecursive
BigInteger factorial(BigInteger n, BigInteger acc = 1) {
    if (n <= 1) return acc
    factorial(n - 1, n * acc)
}

println factorial(20)

The accumulator carries the running product, keeping the recursive call in tail position. The transformation compiles it as a loop, so large inputs no longer exhaust the stack.

$ groovy TailRecursiveDemo.groovy
2432902008176640000

Custom AST Transformations

Beyond the built-in annotations, you can write your own transformation by implementing ASTTransformation and marking it with @GroovyASTTransformation. The class below is a fragment: it is not run as a script. To take effect it must be compiled into a jar, paired with a custom annotation, and registered in META-INF/services/org.codehaus.groovy.transform.ASTTransformation.

LoggingTransform.groovy (fragment)
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation

@GroovyASTTransformation
class LoggingTransform implements ASTTransformation {
    void visit(ASTNode[] nodes, SourceUnit source) {
        ClassNode clazz = nodes[1]
        println "transforming ${clazz.name}"
    }
}

The visit method receives the annotated node and can add methods or fields before compilation finishes. This sample only prints the class name, so it shows the entry point rather than a useful change. Custom transformations are advanced territory, mainly for framework authors and heavy DSL work.

Best Practices for AST Transformations

Source

Groovy Object Orientation Documentation

In this tutorial, we explored Groovy AST transformations through annotations such as @ToString, @EqualsAndHashCode, @Canonical, @Immutable, @Delegate, @Sortable, @Builder, @Grab, and @TailRecursive. Each removes a category of boilerplate at compile time, keeping generated code visible to both the compiler and your tooling.

Author

My name is Jan Bodnar, and I am a passionate programmer with many years of programming experience. I have been writing programming articles since 2007. So far, I have written over 1400 articles and 8 e-books. I have over eight years of experience in teaching programming.

List all Groovy tutorials.