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.
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.
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.
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.
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.
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.
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.
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.
@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.
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.
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
- Prefer Built-ins: Reach for
@Canonicalor@Immutablebefore writing hand-rolled boilerplate. - Order Annotations Deliberately: Some transformations depend on others, so check their documented ordering requirements.
- Pin @Grab Versions: Always specify an exact version to keep scripts reproducible.
- Keep Custom Transforms Narrow: Apply them to a focused concern rather than entire applications.
- Test the Generated Behavior: Assert on equality, sorting, and string output to confirm the transformation did what you expect.
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
List all Groovy tutorials.