Enums & Annotations
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Groovy 6.0 (6.0.0, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26
(checked on 2026-10-14). Groovy enums and annotations are the Java ones, with a few additions: an enum is also a range
endpoint, a String converts to an enum implicitly, an annotation member can hold a closure, and annotations can be
collected into aliases. For the Java side see Java Enums and
Java Annotations & Reflection.
Enums
An enum declares a fixed set of constants. As in Java, constants may take constructor arguments, the enum may have fields and
methods, and it implicitly extends java.lang.Enum. Constants are separated by commas; the list ends at the first member
declaration. The running example models the Season the greenhouse operates in.
enum Season {
SPRING(10, 22), SUMMER(18, 35), AUTUMN(8, 20), WINTER(2, 15)
final int minC, maxC
Season(int minC, int maxC) { this.minC = minC; this.maxC = maxC }
boolean accepts(int temp) { temp in minC..maxC }
static Season of(int month) { values()[((month % 12) / 3) as int] }
}
println Season.SPRING
println Season.values()*.name()
println Season.valueOf('SUMMER').maxC
println Season.SUMMER.ordinal()
println Season.AUTUMN.accepts(12)
println Season.of(7)
println Season.SPRING < Season.SUMMER
println Season.SPRING <=> Season.WINTER
println Season.class.superclass.name
SPRING
[SPRING, SUMMER, AUTUMN, WINTER]
35
1
true
AUTUMN
true
-1
java.lang.Enum
values() returns the constants in declaration order, valueOf(String) looks one up by name and throws
IllegalArgumentException for an unknown name, and ordinal() is the position. Enums are Comparable by ordinal, so
< and <=> work (see Operators: Arithmetic, Relational & Logical).
Groovy 6.0 creates argument-less constants with a direct constructor call, so a meta class registered for the enum before it
initializes no longer sees them; declaring ONE() with an explicit empty argument list restores the old path.
Constant bodies and interfaces
A constant can have its own body that overrides an abstract method of the enum, and an enum can implement interfaces. The
class of a constant with a body is an anonymous subclass (Level$1), while declaringClass still returns the enum.
enum Level {
LOW { String advice() { 'water daily' } },
HIGH { String advice() { 'let it dry' } }
abstract String advice()
}
println Level.LOW.advice()
println Level.HIGH.advice()
println Level.LOW.class.name
println Level.LOW.declaringClass.name
interface Labelled { String label() }
enum Unit implements Labelled {
CELSIUS('C'), FAHRENHEIT('F')
private final String sym
Unit(String sym) { this.sym = sym }
String label() { sym }
}
println Unit.values()*.label()
println Unit.FAHRENHEIT.label()
def counts = [(Level.LOW): 2, (Level.HIGH): 5]
println counts
println Level.values().collectEntries { [(it): it.advice().size()] }
water daily
let it dry
Level$1
Level
[C, F]
F
[LOW:2, HIGH:5]
[LOW:11, HIGH:10]
Ranges of enums
An enum is a valid range endpoint, because Groovy enums have next() and previous() that step through the declaration order.
A range then supports in, toList() and iteration. Both methods wrap around: WINTER.next() is SPRING. See
Ranges.
enum Season { SPRING, SUMMER, AUTUMN, WINTER }
def growing = Season.SPRING..Season.AUTUMN
println growing
println growing.toList()
println Season.SUMMER in growing
println Season.WINTER.next()
println Season.SPRING.previous()
SPRING..AUTUMN
[SPRING, SUMMER, AUTUMN]
true
SPRING
WINTER
String to enum coercion
Groovy converts a String or GString to an enum constant by calling valueOf, wherever the target type is a known enum: an
assignment to a typed variable, a return from a method declared with the enum type, as and a case label. A method call
with a String argument is not coerced: use as. An unknown name throws IllegalArgumentException.
enum Season { SPRING, SUMMER, AUTUMN, WINTER }
Season s = 'WINTER'
println s
def v = 'summer'.toUpperCase()
Season t = "${v}"
println t
println 'AUTUMN' as Season
try { Season bad = 'MONSOON' } catch (IllegalArgumentException e) { println e.message }
Season pick(Season s) {
switch (s) {
case 'SPRING': return Season.SUMMER // String case label compared with the enum
case 'SUMMER': return 'AUTUMN' // implicit coercion on return
default: return Season.WINTER
}
}
println pick('SPRING' as Season) // an argument needs an explicit `as`
println pick(Season.SUMMER)
WINTER
SUMMER
AUTUMN
No enum constant Season.MONSOON
SUMMER
AUTUMN
Source: Groovy documentation: Semantics, String to enum coercion. For the other conversions see Type Coercion & Promotion; for sealed hierarchies as an alternative to enums that carry instance data see Records & Sealed Types.
Annotations
An annotation type is declared with @interface. Its members are methods without a body and an optional default. The allowed
member types are primitives, String, Class, an enum, another annotation, or an array of any of these. Unlike Java, a Groovy
annotation can change the language itself: AST transformations such as @ToString are annotations that generate code (see
AST Transformations: Code Generation & Class Design).
Annotations are placed on classes, methods, fields, parameters, local variables and so on. A usage must set every member that has no
default, and value= can be dropped when value is the only member being set. @Retention decides how long the annotation
survives (SOURCE, CLASS or RUNTIME; only RUNTIME is visible to reflection) and @Target limits where it can be used.
Groovy does not support the TYPE_PARAMETER target introduced in Java 8.
import java.lang.annotation.*
enum Priority { LOW, NORMAL, URGENT }
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.METHOD, ElementType.TYPE])
@interface Task {
String value() // the only mandatory member
int everyMinutes() default 60
Priority priority() default Priority.NORMAL
String[] zones() default []
Class handler() default Object
}
@Task('watering')
class Valve {
@Task(value = 'mist', everyMinutes = 15, priority = Priority.URGENT, zones = ['north', 'south'])
void mist() {}
@Task('log') void log() {}
}
def t = Valve.getAnnotation(Task)
println "${t.value()} ${t.everyMinutes()} ${t.priority()} ${t.zones()} ${t.handler().simpleName}"
def m = Valve.getMethod('mist').getAnnotation(Task)
println "${m.value()} ${m.everyMinutes()} ${m.priority()} ${m.zones().toList()}"
println Valve.getMethod('log').annotations*.annotationType()*.simpleName
println Task.isAnnotation()
println Task.getAnnotation(Retention).value()
watering 60 NORMAL [] Object
mist 15 URGENT [north, south]
[Task]
true
RUNTIME
The annotation is read back with ordinary reflection (getAnnotation), the same API as in Java.
Array initializers (Groovy 6.0)
Groovy 6.0 accepts Java’s brace syntax for array-valued members, both in usages and in default values (GROOVY-11866 and
GROOVY-11845). Earlier versions required a list literal (zones = ['a', 'b']) or an explicit new String[]{...}.
import java.lang.annotation.*
@Retention(RetentionPolicy.RUNTIME)
@interface Task { String value(); String[] zones() default [] }
@Task(value = 'x', zones = {'a', 'b'})
class WithBraces {}
println WithBraces.getAnnotation(Task).zones().toList()
@interface Defaults { String[] names() default {} }
println Defaults.getMethod('names').defaultValue.toList()
[a, b]
[]
Closure annotation parameters
A Groovy annotation member of type Class can be given a closure. The compiler turns the closure into a class and stores that
class in the annotation, so the closure body is not compiled in the context of the annotated element. A processor reads the
annotation, instantiates the closure class and calls it. The example is an @OnlyIf annotation whose closure sees a
jdk variable supplied through its delegate.
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.reflect.Modifier
@Retention(RetentionPolicy.RUNTIME)
@interface OnlyIf { Class value() } // a closure class as the annotation value
class Tasks {
Set result = []
@OnlyIf({ jdk >= 17 })
void modern() { result << 'modern' }
@OnlyIf({ jdk >= 99 })
void future() { result << 'future' }
void always() { result << 'always' }
}
def run(Class taskClass) {
def tasks = taskClass.newInstance()
def env = [jdk: 21]
taskClass.declaredMethods.findAll { Modifier.isPublic(it.modifiers) && !it.synthetic && it.name in ['modern', 'future', 'always'] }
.sort { it.name }.each { m ->
def onlyIf = m.getAnnotation(OnlyIf)
if (onlyIf) {
Closure cl = onlyIf.value().newInstance(tasks, tasks) // instantiate the closure class
cl.delegate = env // resolve `jdk` against our map
cl.resolveStrategy = Closure.DELEGATE_FIRST
if (cl()) m.invoke(tasks)
} else {
m.invoke(tasks)
}
}
tasks
}
println run(Tasks).result.sort()
[always, modern]
The delegation strategy used here is explained on Closure Delegation: Owner, Delegate & This. Contract libraries build on the same mechanism; see Design by Contract with groovy-contracts.
Meta-annotations
A meta-annotation (annotation alias) is an annotation marked @AnnotationCollector that stands for the annotations written on
it. The compiler replaces the alias with the collected annotations during semantic analysis. Arguments given to the alias are
copied to every collected annotation that has a member of that name, so an alias can override a default. Meta-annotations are a
Groovy-only feature: Java code cannot define or use them, but the collected annotations can be Java ones.
import groovy.transform.*
import java.lang.annotation.*
@Retention(RetentionPolicy.RUNTIME) @interface Timeout { int after() default 10 }
@Retention(RetentionPolicy.RUNTIME) @interface Dangerous { String type() default 'none' }
@Timeout(after = 3600)
@Dangerous(type = 'explosive')
@AnnotationCollector
@interface Explosive {}
@Explosive
class Bomb {}
@Explosive(after = 0)
class Dud {}
println Bomb.annotations*.annotationType()*.simpleName.sort()
println Bomb.getAnnotation(Timeout).after()
println Dud.getAnnotation(Timeout).after()
println Dud.getAnnotation(Dangerous).type()
@ToString(includeNames = true)
@EqualsAndHashCode
@AnnotationCollector
@interface Value {}
@Value
class Plant { String name; int zone }
def p = new Plant(name: 'basil', zone: 2)
println p
println p == new Plant(name: 'basil', zone: 2)
[Dangerous, Timeout]
3600
0
explosive
Plant(name:basil, zone:2)
true
The @Value alias in the second half shows the usual use of the feature: bundling several AST transformations under one name.
@RecordType, which the record keyword expands to, is itself such an alias (see
Records & Sealed Types). The collected list can also be passed as
the annotation’s value, @AnnotationCollector([ToString, EqualsAndHashCode]), instead of being written on the alias. Aliases may be
precompiled or in the source tree being compiled.
Improved annotation validation (Groovy 6.0)
Before 6.0, an annotation could be placed on an import or a loop without any check. Groovy 6.0 enforces the @Target rules
there (GROOVY-11884) and follows JLS 9.6.4.1 for the Java targets (GROOVY-11838). It is a breaking change for code that relied on
the lenient behaviour:
@Deprecated import java.lang.String
println 'compiled'
a4.groovy: 1: Annotation @java.lang.Deprecated is not allowed on element IMPORT
@ line 1, column 1.
@Deprecated import java.lang.String
^
1 error
The same check rejects a method-only annotation on a class (Annotation @OnlyMethods is not allowed on element TYPE). Groovy-only
targets are declared with the new @ExtendedTarget meta-annotation and its ExtendedElementType enum, which has the values IMPORT
and LOOP. @Grab, @Newify and @BaseScript are valid on imports, and @Invariant, @Decreases and @Parallel on loops. An
annotation of your own can opt in to loops like this:
import groovy.lang.annotation.ExtendedElementType
import groovy.lang.annotation.ExtendedTarget
import java.lang.annotation.*
@Retention(RetentionPolicy.RUNTIME)
@ExtendedTarget(ExtendedElementType.LOOP)
@interface Hot {}
@Hot
for (i in 1..2) { println i }
1
2
Where the book is outdated
Groovy in Action (publisher page in the references) predates meta-annotations (@AnnotationCollector, Groovy 2.1), closure
annotation parameters in their current form, the Groovy 6.0 annotation validation, and the array initializer syntax. For enums it is
still accurate; check the semantics guide for the coercion rules. See also Classes, Fields & Properties,
Metaclasses, ExpandoMetaClass & Categories for adding behaviour to enums at run time,
and What’s New & Migrating from Older Versions.