What’s New & Migrating from Older Versions
|
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 collects in one place what changed between Groovy releases and how to move code forward. The baseline is Groovy 6.0.0 (released 2026-09-22, JDK 17 or newer); every example on the page was run with Groovy 6.0.0 on JDK 26 on 2026-10-14. Version numbers, dates and maintained lines were checked on that day against the official change log and the release notes. Features introduced after Groovy 4.0 name their release, and a feature described as incubating may still change in a minor release. Each change is explained in depth on the guide page linked from it.
The page has four parts: the release-by-release table and timeline, the legacy-to-modern table, the breaking changes with migration steps and a checklist.
Release by release: 1.0 to 6.0
Groovy follows semantic versioning. Two release numbers never shipped under their original name: 1.1 was renamed 1.5 and 1.9 was renamed 2.0. A 2.6 line was planned as a JDK 7 back-port of the 3.0 parser but was retired after four alpha releases (the last on 2018-06-23), so it is not in the table. A 7.0 release-notes page exists but is marked as under development with no release on the change log; nothing on this page depends on it.
| Release | Date | JDK | Headline features and notes |
|---|---|---|---|
2007-01-03 |
old JDKs (not stated in the notes) |
The language that the first generation of Groovy books describes. There is no release-notes page for 1.0; the change log lists the release. |
|
2007-12-07 |
old JDKs (not stated in the notes) |
Java 5 features: annotations, enums, generics, static imports, varargs, covariant returns; the classic |
|
2009-02-18 |
old JDKs (not stated in the notes) |
Compile-time AST transformations ( |
|
2009-12-22 |
old JDKs (not stated in the notes) |
Anonymous inner classes and nested classes; annotation improvements; power asserts; a rewritten |
|
2011-04-27 |
old JDKs (not stated in the notes) |
Command chains for DSLs; closure composition, trampoline (stack-safe recursion), currying and memoisation
improvements; GPars bundled; native JSON support; more AST transformations ( |
|
2012-06-28 |
JDK 7 features need JDK 7 |
Static type checking and static compilation ( |
|
2013-01-24 |
JDK 7 for indy |
Full |
|
2013-11-18 |
old JDKs (not stated in the notes) |
Implicit closure coercion to single-method interfaces; |
|
2014-05-05 |
runs on JDK 8 |
Traits; a NIO module; much faster JSON parsing and serialisation; closure parameter type inference; the markup
template engine; |
|
2015-01-21 |
old JDKs (not stated in the notes) |
Android support; smaller bytecode and better performance; |
|
2018-05-27 |
JRE 7+, build on JDK 8 |
Macros; |
|
2020-02-07 |
JRE 8+, build on JDK 9 |
The Parrot parser: lambdas, method references, |
|
2022-01-25 |
JRE 8+, tested 8-17 |
|
|
2025-08-21 |
JRE 11+, build on JDK 17, tested 11-25 |
About 350 new GDK methods; JEP 512 instance |
|
2026-08-12 |
JRE 11+, tested 11-25 (EA 26 and 27) |
Console support on JDK 26 and later; security hardening: safer recursive directory deletion, stricter Grape
coordinates, |
|
2026-09-22 |
JRE 17+, tested 17-27 |
Async/await and the |
The rows for 1.5 to 2.5 summarise the corresponding release notes; the notes of the older lines do not state a minimum JDK, so the column says so instead of guessing. Source: Groovy documentation: Release notes and Change logs.
Maintained lines on 2026-10-14
| Line | Latest release on the change log | Minimum JRE |
|---|---|---|
6.0 |
6.0.0 (2026-09-22) |
17 |
5.1 |
5.1.3 (2026-09-19) |
11 |
5.0 |
5.0.8 (2026-07-29), still releasing |
11 |
4.0 |
4.0.33 (2026-07-29) |
8 |
3.0 |
3.0.25 (2025-05-24), the last 3.0 release; nothing for over a year |
8 |
The official download page still described 6.0 as the next major version when this page was checked, while the change log and the 6.0 release notes list 6.0.0 as released. When in doubt, the change log is authoritative. Gradle, Jenkins pipelines and JMeter embed their own Groovy line, which is usually older than 6.0: check which one your tool ships before using a newer feature in its scripts (see Ecosystem and JMeter: Scripting with JSR223 and Groovy).
Timeline
Legacy to modern
Older books, blog posts and Stack Overflow answers describe Groovy 1.x and 2.x. Most of what they teach still works, but several statements are now wrong and many idioms have a better replacement. The first table lists the specific places where a Groovy 1.0 reference (the era of the first generation of printed books) no longer matches Groovy 6.0; the second table lists further legacy-to-modern replacements. The guide pages always show the current form.
Where Groovy 1.0 material is out of date
| Older statement or form | Groovy 6.0 | Page |
|---|---|---|
The classic |
Supported since 1.5; the enhanced forms (several variables in 3.0, an index variable in 5.0) exist too |
|
There is no |
|
|
Java’s package-wide default visibility cannot be expressed |
The |
|
No lambdas, method references, |
All added by the Parrot parser in 3.0; the |
|
Examples avoid annotations, enums, generics, static imports and varargs (they target a JDK that lacks them) |
All supported since 1.5 |
|
Metaprogramming means |
|
|
Everything is dynamic |
Optional static type checking and static compilation ( |
|
JSR 241 makes Groovy the second standard language of the JVM, and Groovy is a Codehaus project |
JSR 241 has been dormant since 2012; Groovy is a top-level Apache Software Foundation project |
|
|
|
|
|
JUnit 5 or 6 with |
|
No built-in dependency management |
Grape and |
|
Swing, Ant and SQL classes live in the core jar |
Separate modules ( |
|
XML-RPC and GroovySOAP for remote calls |
Those modules are no longer maintained; use the JDK |
|
Groovlets run on |
Jakarta Servlet is the default since 5.0; |
|
|
It binds the loopback address unless you give a host, and it has no authentication |
|
The launchers append |
Since 6.0 a classpath you supply is used exactly as written |
|
Sandboxing relies on the Java Security Manager |
Security Manager support was removed in 6.0; use |
|
|
Rewritten on JLine 3 in 5.0 with commands and completion; |
|
Call-site caching is how method dispatch works |
|
|
Concurrency means |
Async/await, parallel collections, actors, agents and dataflow in |
Other legacy idioms and their replacements
| Legacy idiom | Modern replacement | Page |
|---|---|---|
|
|
|
|
Traits, which compose behaviour at compile time with proper inheritance rules |
|
|
Records (4.0, incubating in 6.0), |
|
|
The |
|
|
|
|
Anonymous inner classes and |
Lambdas and method references (3.0); closures still coerce to functional interfaces |
|
|
|
|
|
Switch expressions with |
|
|
Drivers register through |
|
Untyped |
|
|
A parallel library such as GPars for futures |
Native async/await, channels and structured concurrency (6.0, incubating) |
|
Hand-rolled SQL strings in loops |
GINQ queries (4.0, stable in 6.0) and |
|
Hand-written JSON or XML parsing with default factories |
|
|
Reflection tricks to call another language’s code |
Joint compilation and the |
Writing the same code in the old and the new style
The script below uses only forms that Groovy 1.0 did not have or that arrived with the Parrot parser and later. It runs unchanged on Groovy 6.0.0:
// Forms that Groovy 1.0 lacked or that only arrived with the Parrot parser (3.0) and later.
import java.util.function.Function
// classic for loop (1.5), do/while (3.0), Java-style array initializer (3.0)
int[] sizes = [3, 5, 8] as int[]
int[] more = new int[]{4, 6}
def total = 0
for (int i = 0; i < sizes.length; i++) { total += sizes[i] }
int n = 3
do { n-- } while (n > 0)
// lambdas and method references (3.0)
Function<String, Integer> len = s -> s.length()
def upper = 'tomato'.&toUpperCase
// negated operators, identity, Elvis assignment, safe indexing (3.0)
def seed = null
seed ?= 'basil'
List<String> none = null
// try-with-resources (3.0)
String first
try (def r = new StringReader('line1\nline2')) { first = new BufferedReader(r).readLine() }
// 5.0: loop with an index variable
def idx = []
for (int i, var item in ["a", "b", "c"]) { idx << "$i$item" }
// 6.0: val
val final1 = 42
assert total == 16 && more.length == 2 && n == 0
assert len('lettuce') == 7 && upper() == 'TOMATO'
assert 'x' !in ['a', 'b'] && !('x' instanceof Integer)
assert seed == 'basil' && none?[0] == null
assert first == 'line1'
assert idx == ['0a', '1b', '2c']
assert (false ==> true) && !(true ==> false)
assert final1 == 42
println 'modern syntax ok'
Output of the real run:
modern syntax ok
Breaking changes and how to migrate
Groovy keeps minor releases compatible; the breaking changes cluster in the major releases and are listed in the "Breaking changes" section of each release-notes page. The tables below cover the ones that affect typical projects.
Moving to Groovy 3.0
Groovy 3.0 introduced the Parrot parser (enabled by default; the old parser could still be selected by a system property) and, to prepare for the Java module system, duplicated many classes into JPMS-friendly packages. Nothing had to change yet, but this is the release in which you should start moving to the new package names, because 4.0 removes the old ones.
Moving to Groovy 4.0
| Change | What to do | Where |
|---|---|---|
Maven coordinates: the |
Update Gradle, Maven and Ant build files (the Gradle and Maven snippets are below) |
|
Legacy packages removed: the duplicated classes in the old packages are gone |
Replace |
|
Old parser removed: the Antlr 2 based parser no longer exists |
Fix any source that only compiled with the old parser; if you cannot, stay on 3.0 |
|
|
Remove the |
|
|
Add |
|
Modules removed: |
Use the Groovy 3 version of those modules or switch libraries |
|
|
Re-check calls of |
A build file moves like this (build snippets, not run here; the coordinates were checked against Maven Central):
// Gradle: Groovy 3 and earlier
implementation 'org.codehaus.groovy:groovy:3.0.25'
// Gradle: Groovy 4 and later (groovy-all is a POM-only artifact; add the modules you need individually)
implementation 'org.apache.groovy:groovy:6.0.0'
implementation 'org.apache.groovy:groovy-json:6.0.0'
<!-- Maven: replace org.codehaus.groovy with org.apache.groovy -->
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>6.0.0</version>
<type>pom</type>
</dependency>
The package move is the most common 4.0 failure. This is the real compiler message for the old name on Groovy 6.0.0:
def xml = '<crops><crop name="basil"/><crop name="mint"/></crops>'
def root = new groovy.util.XmlSlurper().parseText(xml)
println root.crop.size()
legacy-package.groovy: 2: unable to resolve class groovy.util.XmlSlurper
@ line 2, column 12.
def root = new groovy.util.XmlSlurper().parseText(xml)
^
1 error
With the groovy.xml package the same script runs:
def xml = '<crops><crop name="basil"/><crop name="mint"/></crops>'
def root = new groovy.xml.XmlSlurper().parseText(xml)
println root.crop.size()
2
Moving to Groovy 5.0
The runtime minimum rises to JDK 11. The notable breaking changes:
| Change | What to do | Where |
|---|---|---|
Duplicate imports are errors: two imports of the same simple name, or an import and a class with that name, used to let the last one win |
Remove or alias the duplicates |
|
|
Rename them, move them into a package or alias the import |
|
|
Test |
|
|
Rename an overload named |
|
Scripts with only a static |
Turn them into plain scripts or use an instance |
|
Visibility and property access tightened for package-private members, anonymous classes and map-like classes; |
Compile and fix; framework authors should re-test AST transform annotations |
|
Iterator extension methods such as |
Add |
The duplicate-import rule is easy to see. On Groovy 6.0.0 this script fails to compile:
import java.util.Date
import java.sql.Date
println Date
dup-import.groovy: 2: The name Date is already declared @ line 2, column 8.
import java.sql.Date
^
1 error
The File truthiness change and its porting flag, run on Groovy 6.0.0 (the second run adds the system property):
def missing = new File('no-such-file.txt')
println missing.exists()
println(missing ? 'truthy' : 'falsy')
println(missing != null ? 'set' : 'unset')
false
falsy
set
false
truthy
set
An overload named remainder is now what % calls:
class Tray {
int n
Tray remainder(int k) { new Tray(n: n % k) }
String toString() { "Tray($n)" }
}
println new Tray(n: 17) % 5
Tray(2)
Moving to Groovy 5.1
Groovy 5.1 keeps the 5.0 JDK requirements. Its breaking changes are small and security-motivated:
-
deleteDir()onFileandPathtreats symbolic links as leaves instead of following them into their targets. If you relied on deleting the contents of a linked directory, resolve the link and delete the target yourself. -
Grape coordinates containing
..sequences are rejected. -
The deprecated
run(JApplet)method was removed from the Groovy Console so that it loads on JDK 26 and later.
Moving to Groovy 6.0
The runtime minimum rises to JDK 17 (Groovy 6.0 was tested on JDK 17 to 27). The headline breaking changes:
| Change | What to do | Where |
|---|---|---|
Security Manager support removed: Groovy no longer calls |
Stop relying on policy files; restrict scripts with |
|
The launchers no longer add |
Add |
|
Switch: position decides: a |
Add a |
|
Generic types must be well formed: generic array creation, a generic class extending |
Use the workaround shown in the Generics section of the release notes (for example |
|
|
Rename a field or variable named |
|
|
Use |
|
A |
Nothing, unless your code relied on the final value; |
|
The classic call-site runtime moved to the optional |
Add the module only if you run with |
|
XML is secure by default; |
Use the documented opt-out properties only as porting aids |
|
String-to- |
Call |
|
Method-level |
Remove the method annotation or give it |
The classpath dot
A script that loads a class from the working directory while you also pass -cp for a library used to work because the
launcher appended the current directory. With 6.0 the classpath you pass is used as is. The demonstration compiles a class
Watering into the current directory and runs use.groovy, which calls it, three ways:
groovyc Watering.groovy
groovy use.groovy # no classpath given: falls back to the current directory
groovy -cp /nonexistent-dir use.groovy # explicit classpath: the current directory is not added
groovy -cp "/nonexistent-dir:." use.groovy
water at dawn
SEVERE: Caught: {0}
groovy.lang.MissingPropertyException: No such property: Watering for class: use
...
water at dawn
Replacing the Security Manager
Setting a Security Manager is not possible on current JDKs, and Groovy 6 no longer cooperates with one. On JDK 26 the call fails immediately:
println System.getSecurityManager()
try { System.setSecurityManager(null) } catch (e) { println e.class.name }
null
java.lang.UnsupportedOperationException
For scripts that you do not fully trust, restrict what they may compile with a SecureASTCustomizer (and still run them in a
container or an unprivileged operating-system account, because a compile-time allow-list is not a full sandbox):
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.SecureASTCustomizer
import org.codehaus.groovy.control.MultipleCompilationErrorsException
def secure = new SecureASTCustomizer()
secure.with {
// allow-list of receiver types for method calls: System.exit(...) is not on it
allowedReceiversClasses = [Object, Integer, String, Math]
}
def config = new CompilerConfiguration()
config.addCompilationCustomizers(secure)
def shell = new GroovyShell(config)
println shell.evaluate('Math.max(2, 7)')
try {
shell.evaluate('System.exit(1)')
} catch (MultipleCompilationErrorsException e) {
println 'rejected at compile time: ' + e.errorCollector.getError(0).cause.message.readLines().first()
}
7
rejected at compile time: Method calls not allowed on [java.lang.System]
The switch position rule
Whether a switch is a statement or an expression no longer depends on the arrow: it depends on whether the value is used.
A switch that ends a method is a statement whose matched value is returned by the implicit return (and is null
when nothing matches, as in 4.x and 5.x); a switch on the right of an assignment is an expression and must be exhaustive:
def label(int i) {
switch (i) {
case 1 -> 'one'
case 2 -> 'two'
}
}
println label(1)
println label(9)
def viaExpr = { int i -> def r = switch (i) { case 1 -> 'one'; default -> 'many' }; r }
println viaExpr(7)
try {
def s = 9
def r = switch (s) { case 1 -> 'one' }
} catch (IllegalStateException e) { println "expression with no match: ${e.class.simpleName}" }
one
null
many
expression with no match: IllegalStateException
Generic syntax
Groovy 4 and 5 accepted generic constructs that Java rejects by quietly treating them as raw types. Groovy 6 applies the Java rules in every compilation unit, scripts included. The real error for generic array creation:
List<String>[] buckets = new List<String>[3]
generics.groovy: 1: generic array creation of List<java.lang.String>
@ line 1, column 26.
List<String>[] buckets = new List<String>[3]
^
Use a List<List<String>> or a raw array with a cast instead. Source:
Groovy documentation: Generic type syntax
Two behaviours that change silently
Two 6.0 changes compile and run without any message, so they deserve a test in your suite. The first is the in operator
on a map, which now tests keys; the second is the for-in capture rule:
def stock = [basil: 0, mint: 3]
println('basil' in stock) // key-based since 6.0
println(0 in stock) // values are no longer tested
println(0 in stock.values())
true
false
true
def tasks = []
for (crop in ['basil', 'mint', 'sage']) { tasks << { crop } }
println tasks*.call()
[basil, mint, sage]
With -Dgroovy.forin.per.iteration.capture=false the same script prints [sage, sage, sage], which is what earlier releases
did (every closure shared one binding).
Upgrade checklist
-
Pick the target line. Use the maintained-lines table above; a project on JDK 8 can stay on 4.0, JDK 11 on 5.x, JDK 17 or later can take 6.0.
-
Upgrade one major at a time if you are far behind (3.0, then 4.0, then 5.0, then 6.0): each release-notes page lists the breaking changes for that step only.
-
Fix coordinates and imports first. Switch
org.codehaus.groovytoorg.apache.groovy, move legacy packages to theirgroovy.xml,groovy.ant,groovy.testand similar homes, and add any optional module thatgroovy-allno longer includes. -
Build and run the tests on the new line. Read compile errors against the tables above; run with the documented porting flags only to separate behavioural changes from syntax changes, and remove them afterwards.
-
Check the toolchain. Gradle, Maven plugins, IDEs, Jenkins and JMeter embed their own Groovy line; see Build Tools: Gradle, Maven & Ant.
-
Re-check tests and frameworks. Spock publishes one artifact per Groovy line (for example
2.5-groovy-6.0), see Testing with Spock & Geb. -
Adopt new features gradually.
var,val, switch expressions, records and@CompileStaticare low risk; treat async/await and other incubating features as subject to change.
For the Java side of the same questions (language levels, the module system and build tooling) see Java: Build & Tooling and Java: Packages & Modules.
References
-
Groovy in Action (first edition), Manning Publications, consulted for the Groovy 1.0 baseline described in the "out of date" table