Groovy @Canonical: Generate Value-Object Boilerplate at Compile Time
Use @groovy.transform.Canonical to generate getters, setters, equals, hashCode, and toString for Groovy classes without writing them by hand.
06 Jun 2026, 19:34 UTC

Start with the useful answer
If you write Groovy classes that exist mainly to hold data, you still end up writing getters, setters, equals, hashCode, and toString by hand. Groovy's @groovy.transform.Canonical annotation removes that boilerplate. Annotate the class, and the Groovy compiler generates those methods during compilation instead of requiring you to maintain them manually.
How the mechanism works
@Canonical is a compile-time AST (abstract syntax tree) transformation. An AST transformation inspects and modifies the program's structure while it is being compiled. When the compiler sees the annotation on a class, it synthesizes property accessors and standard Object methods directly into the bytecode. The source file stays short, but the compiled class behaves as if you had written the methods yourself.
Worked configuration
Suppose you need a value object for a product catalog item:
import groovy.transform.Canonical
import java.math.BigDecimal
@Canonical
class Product {
String sku
BigDecimal price
int stockQuantity
}
Because the fields are declared as properties, the compiler generates getSku, setSku, getPrice, setPrice, getStockQuantity, and setStockQuantity. It also generates equals, hashCode, and toString based on those properties.
You can then construct and compare instances without writing any of those methods:
def item = new Product(sku: 'ABC-123', price: new BigDecimal('19.99'), stockQuantity: 10)
def duplicate = new Product(sku: 'ABC-123', price: new BigDecimal('19.99'), stockQuantity: 10)
println item.sku
println item.toString()
println item == duplicate
The map-based constructor works because Groovy recognizes the generated properties. The equality check compares the property values rather than object identity.
Verify what the compiler generated
Compile the source and inspect the class file with the Java disassembler:
groovyc Product.groovy
javap -public Product.class
Run these commands in a terminal from the directory containing the source and output files. You need read access to Product.groovy and write access to the output directory. The placeholders are your source filename and the resulting class file. A successful check shows public method signatures for the property accessors and for equals, hashCode, and toString. If those signatures are missing, the annotation was not applied or the class does not declare eligible properties. This step carries no runtime risk; it only inspects compiled output.
Limits of @Canonical
- The annotation works on classes with properties. It does not generate methods for plain local variables or non-property fields.
- It generates setters, so the class is mutable by default. Do not use it if you need an immutable value object unless you remove or override the generated setters.
- It does not let you define a custom canonical constructor; the generated constructor follows the property order and map-based initialization style.
- Combining
@Canonicalwith@groovy.transform.ToString,@EqualsAndHashCode, or similar transforms can produce duplicate method definitions and compilation errors.
Common mistakes
- Expecting immutability. Generated setters allow callers to change state after construction. If your domain model requires read-only objects, use an immutable pattern or a different transform instead.
- Stacking transforms. Adding
@ToStringon top of@Canonicaldoes not customize output; it risks duplicatetoStringmethods. Choose one transform for each method family. - Assuming subclass behavior. Because the compiler generates methods based on the parent class properties, a subclass that adds its own properties may not include them in inherited
equalsorhashCodeimplementations without explicit overrides.
Practical check before adopting it
After compiling, run a short Groovy script that constructs two instances with identical property values, calls toString, and compares them with ==. Confirm that toString includes the expected property names and that equality returns true. If the script fails, check that the class is annotated, that the fields are properties, and that no conflicting transform is present.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.