Release notes for Groovy 6.0
Groovy 6 builds upon existing features of earlier versions of Groovy. In addition, it incorporates numerous new features and streamlines various legacy aspects of the Groovy codebase.
|
Highlights
Native Async/Await (incubating) (see Native Async/Await (incubating))
-
Sequential-style concurrent code — no callbacks or
CompletableFuturechains. -
Automatic virtual threads on JDK 21+; cached thread pool fallback on JDK 17–20.
-
Generators with
yield return, Go-style channels,for await, structured concurrency viaAsyncScope.
Integrated Concurrency Toolkit (incubating) (see Integrated Concurrency and Parallel Processing (incubating))
-
Unified
groovy.concurrentpackage: agents, actors, dataflow variables, channels, and parallel collections. -
@ActiveObjectadds actor semantics to ordinary classes — no message protocols to hand-write. -
Parallel
Collectionmethods (collectParallel,findAllParallel,eachParallel, …) for CPU-bound work. -
Same APIs available to Java, Kotlin and other JVM languages via the standalone
groovy-concurrent-javamodule (see Java-only module:groovy-concurrent-java).
New HTTP Client Module (incubating) (see HttpBuilder: HTTP Client Module (incubating))
-
groovy-http-builderoffers both an imperative DSL and a declarative@HttpBuilderClientinterface. -
Built on the JDK’s
java.net.http.HttpClientwith native async support. -
Auto-parsed JSON, XML and HTML responses; typed return objects driven by interface signatures.
New Language Features
-
valcontextual keyword for final declarations — a clean companion tovar(seevalKeyword for Final Declarations). -
Module imports (JEP 511):
import module java.sqlcovers every exported package in one line (see Module Import Declarations). -
Destructuring with rest binders (
def (h, *t) = list) and map-style keys (def (name: n) = person) (see Multi-assignment Destructuring). -
Compound-assignment operator overloading (
plusAssign,minusAssign, …) for efficient in-place mutation, even onfinalfields (see Compound Assignment Operator Overloading). -
AST transforms now valid on loop statements —
@Parallelfor-loops,@Invariant,@Decreases(see AST Transforms in More Places (incubating)).
Designed for Human and AI Reasoning (see Designed for Human and AI Reasoning)
-
New
NullCheckertype checker with an optional flow-sensitivestrictmode requiring no annotations (see Type Checking Extensions). -
@Modifiesframe conditions and@Purepurity declarations, verified at compile time byModifiesCheckerandPurityChecker. -
Loop invariants and termination measures via
@Invariant/@Decreases(see Groovy-Contracts Enhancements (incubating)). -
Contracts (
@Requires,@Ensures,@Invariant) now also work in scripts. -
Each method becomes a self-contained specification — readable without descending into bodies.
Joint Compilation Stub Improvements (incubating) (see Joint Compilation Stub Improvements (incubating))
-
AST-transform-generated members (
@Immutable,@Builder,@TupleConstructor,@Delegate, …) are now visible in generated stubs. -
Java code in mixed-language projects can finally call constructors and methods contributed by transforms.
New Optional Modules (see New Modules)
-
groovy-concurrent-java— Standalone Java library exposing thegroovy.concurrenttoolkit; no Groovy runtime required (see Java-only module:groovy-concurrent-java). -
groovy-http-builder— HTTP client with imperative DSL and declarative@HttpBuilderClientinterface (see HttpBuilder: HTTP Client Module (incubating)). -
groovy-csv— RFC 4180 CSV reading/writing with optional Jackson-backed typed parsing (see CSV Module (incubating)). -
groovy-markdown— CommonMark parser with section, code-block and table extraction helpers (see Markdown Module (incubating)). -
groovy-grape-ivy—@GrabIvy backend, now its own optional module (previously bundled in core) (see Grape: Dual Engine Support (incubating)). -
groovy-grape-maven—@Grabpowered by Maven Resolver, alongside the existing Ivy engine (see Grape: Dual Engine Support (incubating)). -
groovy-reactor/groovy-rxjava—awaitandfor awaitover reactiveMono/Flux/Observabletypes. -
groovy-test-junit6— Run JUnit Jupiter 6 tests as Groovy scripts (see JUnit 6 Support).
Extension Method Additions (see Extension method additions and improvements)
-
New methods including
groupByMany,waitForResult,findGroups/findAllGroups,isSorted, and lazygrepping. -
Asynchronous file I/O on
Path(textAsync,bytesAsync,writeAsync) returningCompletableFuture— composes withawait. -
Streamlined process handling:
pipeline,onExit,toProcessBuilder, named-parameterexecute(dir:, env:, …). -
Finer-grained
groovy.extension.disable— target single overloads by parameter signature (see Selectively Disabling Extension Methods).
GINQ and Data Format Improvements (see GINQ Enhancements, Typed Parsing and Writing Across Format Modules)
-
GINQ
groupby … intobinds each group to a named variable with aggregate access. -
SQL-style set operators in GINQ:
union,intersect,minus,unionall. -
Consistent typed parsing across JSON, CSV, TOML, YAML and XML modules.
GroovyDoc and Tooling Improvements (see GroovyDoc Enhancements, Theming Improvements)
-
JEP 467 Markdown doc comments (
///) and JEP 413{@snippet}blocks for inline and external code samples. -
Prism.js syntax highlighting, class-hierarchy tree pages,
{@value}/{@inheritDoc}support, script documentation. -
Light, dark, "follow system" and custom themes for GroovyDoc, the GDK reference, and GroovyConsole.
-
GroovyConsole gains a Set Script Arguments UI option (see GroovyConsole: Script Arguments).
Other Improvements
-
groovyToString()protocol for customising display in string interpolation,printlnand collection formatting (see Customisable Object Display withgroovyToString). -
Platform Logging API: Groovy diagnostics route through standard JVM logging (SLF4J, Log4j2, JUL, …) (see Platform Logging).
-
JUnit 6 (Jupiter 6) support (see JUnit 6 Support).
-
Improved annotation target validation on import and loop statements (see Improved Annotation Validation).
Broader JDK Support (see JDK requirements)
-
Tested across JDK 17–26; JDK 17 is now the minimum supported runtime.
-
Security Manager support removed in line with JEP 411.
New Modules
Groovy 6 ships eight new optional modules, plus a long-standing piece of core (Grape’s Ivy backend) is split out into its own module:
| Module | Purpose |
|---|---|
|
Standalone Java library exposing the |
|
CSV parsing and writing via Jackson CSV |
|
|
|
New |
|
Imperative DSL and declarative annotation-driven client over JDK
|
|
CommonMark Markdown parser |
|
AwaitableAdapter SPI for
Project Reactor ( |
|
|
|
Groovy runner for JUnit 6 (Jupiter) tests as scripts |
Each module has a dedicated section below covering its public API, examples, and any migration notes.
Native Async/Await (incubating)
Groovy 6 adds native async/await support
(GROOVY-9381),
enabling developers to write concurrent code in a sequential, readable style — no callbacks, no java.util.concurrent.CompletableFuture chains, no manual thread management.
On JDK 21+, tasks automatically leverage virtual threads.
See also the async/await blog post for a detailed walkthrough.
Before and after
Without async/await, concurrent code requires chaining futures:
// Before: CompletableFuture chains
def future = CompletableFuture.supplyAsync { loadUserProfile(id) }
.thenCompose { profile -> CompletableFuture.supplyAsync { loadQuests(profile) } }
.thenApply { quests -> quests.find { it.active } }
def quest = future.join()
With async/await, the same logic reads like synchronous code:
// After: sequential style, concurrent execution
def quest = await async {
def profile = await async { loadUserProfile(id) }
def quests = await async { loadQuests(profile) }
quests.find { it.active }
}
Exception handling works with standard try/catch — no .exceptionally() chains.
Parallel tasks and combinators
Launch tasks concurrently and coordinate results:
def a = async { fetchFromServiceA() }
def b = async { fetchFromServiceB() }
def c = async { fetchFromServiceC() }
// Wait for all three
def (resultA, resultB, resultC) = await(a, b, c)
Generators with yield return
An async closure containing yield return becomes a lazy generator — it produces values on demand with natural back-pressure:
def fibonacci = async {
long a = 0, b = 1
while (true) {
yield return a
(a, b) = [b, a + b]
}
}
assert fibonacci.take(8).collect() == [0, 1, 1, 2, 3, 5, 8, 13]
Channels
Go-style inter-task communication. A producer sends values into a channel (AsyncChannel); a consumer receives them:
def ch = AsyncChannel.create(5) // buffered channel
async {
for (i in 1..10) ch.send(i)
ch.close()
}
for (val in ch) { println val } // prints 1..10
Structured concurrency
AsyncScope binds the lifetime of child tasks to a scope — when the scope exits, all children are guaranteed complete or cancelled:
AsyncScope.run {
def users = async { loadUsers() }
def config = async { loadConfig() }
processResults(await(users), await(config))
}
// Both tasks guaranteed complete here
Feature summary
| Feature | Description |
|---|---|
|
Start background tasks; collect results in sequential style |
Virtual threads |
Automatic on JDK 21+; cached thread pool fallback on JDK 17—20 |
Awaitable |
Wait for all tasks to complete |
|
Race — first to complete wins |
|
First success wins (ignores individual failures) |
|
Wait for all; inspect each outcome individually |
|
Lazy generators with back-pressure |
Buffered and unbuffered Go-style channels |
|
|
Iterate over async sources (generators, channels, reactive streams) |
|
LIFO cleanup actions, runs on scope exit regardless of success/failure |
|
Structured concurrency — child lifetime bounded by scope |
Timeouts |
|
|
Non-blocking pause |
|
|
Framework adapters (SPI) |
|
Executor configuration |
Pluggable; default auto-selects virtual threads or cached pool |
Integrated Concurrency and Parallel Processing (incubating)
Groovy 6 brings a unified concurrency and parallel-processing toolkit
into core under
GEP-18: Integrated Concurrency and Parallel Processing
(GROOVY-11952,
GROOVY-11953).
The new abstractions live in the groovy.concurrent package and
modernise patterns from
GPars around virtual threads, structured concurrency,
and Groovy 6’s async/await. The same combinators
(await, Awaitable.all, for await) work uniformly across all of
them — agents, actors, dataflow variables, channels, and parallel
collections — so you compose features rather than learning separate
APIs.
For Java-only consumers, the same APIs are also published as the
new groovy-concurrent-java module — see Java-only module: groovy-concurrent-java.
Agents — thread-safe mutable state
An Agent wraps a value and serialises
updates via functions, eliminating data races by design. Reads compose
with await:
import groovy.concurrent.Agent
def counter = Agent.create(0)
counter.send { it + 1 }
counter.send { it + 1 }
counter.send { it + 1 }
assert await(counter.getAsync()) == 3
An agent also exposes its update stream as a
java.util.concurrent.Flow.Publisher via changes(), so for await
consumes state transitions directly:
def agent = Agent.create(0)
async {
3.times { agent.send { it + 1 } }
agent.shutdown()
}
def seen = []
for await (v in agent.changes()) { seen << v }
assert seen == [1, 2, 3]
@ActiveObject — actor semantics with class syntax
A hand-written actor (Actor) expresses concurrency by encoding a message
protocol — typed messages, a switch over message kinds, and explicit
send/sendAndGet plumbing at every call site:
import groovy.concurrent.Actor
// Hand-written actor — message protocol explicit
def account = Actor.stateful(0.0) { balance, msg ->
switch (msg) {
case { it instanceof Map && it.deposit }:
return balance + msg.deposit
case { it instanceof Map && it.withdraw }:
if (msg.withdraw > balance) throw new RuntimeException('Insufficient funds')
return balance - msg.withdraw
default: return balance
}
}
account.send([deposit: 100])
account.send([withdraw: 30])
def balance = await(account.sendAndGet([deposit: 0]))
assert balance == 70.0
To know whether the actor is being used safely, a reader (human or AI)
has to follow each message kind through the dispatch loop and match it
against every call site. @ActiveObject
(applicable target: TYPE) inverts this: write a normal class, mark methods that participate in
the actor’s serialised mailbox with
@ActiveMethod (applicable target: METHOD), and the AST
transform routes those calls through an internal actor:
import groovy.transform.ActiveObject
import groovy.transform.ActiveMethod
@ActiveObject
class Account {
private double balance = 0
@ActiveMethod
void deposit(double amount) { balance += amount }
@ActiveMethod
void withdraw(double amount) {
if (amount > balance) throw new RuntimeException('Insufficient funds')
balance -= amount
}
@ActiveMethod
double getBalance() { balance }
}
def account = new Account()
account.deposit(100)
account.deposit(50)
account.withdraw(30)
assert account.getBalance() == 120.0
The thread-safety contract is now explicit and local. The
@ActiveObject annotation declares the concurrency model;
@ActiveMethod bodies contain plain business logic; callers see
ordinary method calls — no message types to invent, no switch to
parse, no manual reply plumbing. For non-blocking use,
@ActiveMethod(blocking = false) returns an Awaitable that plugs
straight into await and Awaitable.all.
Dataflow variables
A DataflowVariable is a
single-assignment variable: any thread that reads before it is bound
blocks until a value is available. DataflowVariable implements
Awaitable, so it composes directly with await and async {}
regardless of which task binds first:
import groovy.concurrent.DataflowVariable
def x = new DataflowVariable()
def y = new DataflowVariable()
def z = new DataflowVariable()
async { z << await(x) + await(y) } // blocks until x and y bind
async { x << 10 } // bind in any order
async { y << 5 }
assert await(z) == 15
The companion Dataflows class
auto-creates variables on property access for an even more concise
form (async { df.fullName = "${df.first} ${df.last}" }).
Parallel collections
For CPU-bound data parallelism, Collection gains a family of
parallel methods backed by a ForkJoinPool:
def squares = (1..1_000).toList().collectParallel { it * it }
def adults = people.findAllParallel { it.age >= 18 }
def total = amounts.sumParallel { a, b -> a + b }
ParallelScope.withPool
binds a pool for a block, and Pool
offers Pool.cpu(), Pool.fixed(n), Pool.io(), and
Pool.virtual() factories so CPU-bound and I/O-bound workloads can
use distinct pools without leaking into the common pool. The
previously introduced @Parallel loop
annotation shares this infrastructure:
import groovy.concurrent.ParallelScope
import groovy.concurrent.Pool
ParallelScope.withPool(Pool.cpu()) { scope ->
def hot = bigList.findAllParallel { it.score > threshold }
hot.eachParallel { archive(it) }
}
As a rule of thumb, prefer parallel collections (or @Parallel) for
CPU-bound work and async/await with virtual threads for
I/O-bound work — ForkJoinPool workers are precious and should not
be tied up in Thread.sleep, network calls, or blocking I/O.
Channel composition and broadcast
AsyncChannel (introduced with async/await) gains composable
pipeline operations — filter, map, merge, split, tap — and
ChannelSelect for Go-style
multi-channel selection.
BroadcastChannel adds
one-to-many delivery and exposes asPublisher() for direct
Flow.Publisher interop, so any subscriber — including a for await
loop — receives every message:
import groovy.concurrent.BroadcastChannel
def broadcast = BroadcastChannel.create()
def publisher = broadcast.asPublisher()
async {
['hello', 'world'].each { broadcast.send(it) }
broadcast.close()
}
for await (msg in publisher) { println msg } // hello, world
The same for await works against any JDK Flow.Publisher, Reactor
Flux (via groovy-reactor), or RxJava Observable (via
groovy-rxjava).
Java-only module: groovy-concurrent-java
For Java, Kotlin, and other JVM languages that want the same
toolkit without the full Groovy runtime, the standalone
groovy-concurrent-java module exposes AsyncScope, Pool,
ParallelScope, Actor, Agent, DataflowVariable,
AsyncChannel, BroadcastChannel, and ChannelSelect against
plain java.util.function types. The Groovy-only sugar
(async/await keywords, for await, defer, @ActiveObject,
Dataflows, parallel extension methods) requires the full Groovy
dependency.
import groovy.concurrent.AsyncScope;
import org.apache.groovy.runtime.async.AsyncSupport;
var result = AsyncScope.withScope(scope -> {
var a = scope.async(() -> fetchUser(id));
var b = scope.async(() -> fetchOrders(id));
return Map.of(
"user", AsyncSupport.await(a),
"orders", AsyncSupport.await(b)
);
});
The module ships under the org.apache.groovy:groovy-concurrent-java
coordinate and is mutually exclusive with the full groovy runtime
(Gradle enforces this via a shared capability; a runtime warning
fires if both jars are detected on the classpath).
Feature summary
| Feature | Description | Ticket |
|---|---|---|
|
Thread-safe mutable value updated via serialised functions; exposes
a |
|
|
Stateless and stateful actors with |
|
|
AST-driven actor semantics with class syntax. |
|
|
Single-assignment variables; |
|
|
Composable channel pipelines. |
|
|
One-to-many broadcast (with |
|
Parallel collection methods |
|
|
|
|
|
|
Standalone Java module exposing the same concurrent APIs without the Groovy runtime. |
HttpBuilder: HTTP Client Module (incubating)
Groovy 6 introduces a new groovy-http-builder module
(GROOVY-11879,
GROOVY-11924)
providing both an imperative DSL and a declarative annotation-driven
client over the JDK’s java.net.http.HttpClient.
It is designed for scripting, automation, and typed API clients,
filling the gap left by the earlier HttpBuilder/HttpBuilder-NG libraries.
The two entry points are HttpBuilder
for the imperative DSL and
@HttpBuilderClient for the
declarative client; supporting types live in the groovy.http package.
Applicable targets for the new annotations: @HttpBuilderClient — TYPE;
@Get/@Post/@Put/@Delete/@Patch/@Form/@Timeout — METHOD;
@Body/@BodyText/@Query — PARAMETER;
@Header — TYPE, METHOD.
Imperative DSL
A closure-based DSL for quick scripting:
import static groovy.http.HttpBuilder.http
def client = http('https://api.github.com')
def result = client.get('/repos/apache/groovy')
assert result.json.license.name == 'Apache License 2.0'
Responses auto-parse by content type: result.json, result.xml,
result.html (via jsoup), or result.parsed for auto-dispatch.
Declarative client
Define a typed interface and Groovy generates the implementation at compile time. Parameters are mapped by convention — no annotations needed for the common case:
@HttpBuilderClient('https://api.example.com')
interface UserApi {
@Get('/users/{id}')
User getUser(String id) // path param: {id}
@Get('/users')
List<User> search(String name) // implied query param: ?name=...
@Post('/users')
User create(@Body Map user) // JSON body
@Post('/login')
@Form
Map login(String username, String password) // form-encoded
}
def api = UserApi.create()
def user = api.getUser('42')
Async support
Both sides offer native async via HttpClient.sendAsync() — no
extra threads consumed while waiting:
// Imperative
def future = client.getAsync('/slow-endpoint')
def result = future.get()
// Declarative
@HttpBuilderClient('https://api.example.com')
interface AsyncApi {
@Get('/data/{id}')
CompletableFuture<Map> getData(String id)
}
These CompletableFuture returns are first-class await targets in
Groovy 6: def data = await api.getDataAsync('42').
Feature summary
| Feature | Imperative | Declarative |
|---|---|---|
HTTP methods (GET, POST, PUT, DELETE, PATCH) |
All |
|
JSON body / response |
|
@Body / return-type driven |
Form-encoded body |
|
|
Plain text body |
|
|
XML / HTML response |
|
|
Typed response objects |
Manual ( |
Automatic (return type driven) |
Query parameters |
|
Implied from parameter name (or @Query) |
Path parameters |
Manual |
Auto-mapped via |
Headers |
|
@Header on interface/method |
Async |
|
|
Timeouts (connect / request) |
Config DSL |
|
Per-method timeout |
Per-request |
@Timeout |
Redirect following |
Config DSL |
|
Error handling |
Manual (check |
Auto-throw; custom exception via |
JDK client access (auth, SSL, proxy) |
|
|
AST Transforms in More Places (incubating)
Groovy 6 extends the AST transformation infrastructure to support
annotations on loop statements — for-in, classic for, while, and do-while
(GROOVY-11878).
The following built-in transforms now declare LOOP as a valid target:
-
@Parallel — runs each iteration on its own task; uses virtual threads on JDK 21+ and falls back to platform threads otherwise (applicable target:
LOOPonly) -
@Invariant — asserts a condition at the start of each iteration (also valid on imports; see Groovy-Contracts Enhancements (incubating))
-
@Decreases — loop termination measure; see Groovy-Contracts Enhancements (incubating)
-
@ASTTest — compile-time AST inspection utility used primarily for transform development and testing
@Parallel is the most visible application:
@Parallel
for (int i in 1..4) {
println i ** 2
}
// Output (non-deterministic order): 1, 16, 9, 4
Custom transforms can opt into loop targeting by declaring
@ExtendedTarget(ExtendedElementType.LOOP), following the same
contract as class/method/field-level transforms. See
Improved Annotation Validation for the corresponding compile-time
validation rules.
Joint Compilation Stub Improvements (incubating)
This work is specified by GEP-21.
When mixing Groovy and Java sources, Groovy generates Java stubs so
javac can compile Java files that reference Groovy classes.
Historically, those stubs were generated before AST transforms ran, so
members contributed by transforms such as @TupleConstructor,
@Immutable, or @Builder were absent from the stubs and Java code
relying on them failed to compile.
Groovy 6 extends the AST transform framework so that opt-in transforms can contribute member signatures (constructors, methods, fields) to the generated stubs. The change is strictly additive — transforms that do not opt in behave exactly as before (GROOVY-11976).
For example, a Groovy class using @Immutable:
// UserAccount.groovy
@groovy.transform.Immutable
class UserAccount {
String name
int age
}
is now visible to Java callers during joint compilation:
// Caller.java
UserAccount user = new UserAccount("alice", 30);
A wide range of built-in transforms have been updated to contribute
stubs, including @AutoClone, @AutoImplement, @Bindable, @Builder,
@Delegate, @EqualsAndHashCode, @ExternalizeMethods, @Final,
@Immutable, @IndexedProperty, @InheritConstructors, @Lazy,
@ListenerList, @MapConstructor, @NamedVariant, @RecordType,
@Singleton, @Sortable, @ToString, @TupleConstructor, and
@Vetoable. Custom transforms can opt in via one of three shapes
(annotation attribute, marker interface, or split-transform classes).
See GEP-21 for the full specification.
Groovy-Contracts Enhancements (incubating)
The groovy-contracts module receives several enhancements in Groovy 6,
including support for contracts in scripts, loop annotations,
and frame conditions.
Contracts in scripts
Contract annotations now work in Groovy scripts, not just inside classes (GROOVY-11885). @Requires and @Ensures can be placed on script methods, and @Invariant can be placed on an import statement to apply as a class-level invariant for the script:
@Invariant({ balance >= 0 })
import groovy.transform.Field
import groovy.contracts.Invariant
@Field Integer balance = 5
@Requires({ balance >= amount })
def withdraw(int amount) { balance -= amount }
def deposit(int amount) { balance += amount }
deposit(5) // balance = 10, OK
withdraw(20) // throws ClassInvariantViolation (balance would be -5)
Combining contracts
These annotations can be combined to build strong confidence in the correctness of an algorithm. Consider this insertion sort that merges two pre-sorted lists:
@Ensures({ result.isSorted() })
List insertionSort(List in1, List in2) {
var out = []
var count = in1.size() + in2.size()
@Invariant({ in1.size() + in2.size() + out.size() == count })
@Decreases({ [in1.size(), in2.size()] })
while (in1 || in2) {
if (!in1) return out + in2
if (!in2) return out + in1
out += (in1[0] < in2[0]) ? in1.pop() : in2.pop()
}
out
}
The @Ensures postcondition verifies that the result is sorted.
The @Invariant asserts that no elements are lost or gained — the total number of elements across all three lists stays constant
throughout the loop. The @Decreases annotation uses a lexicographic
measure over the two input list sizes, giving us confidence that
the loop terminates: on each iteration at least one input list
shrinks, and they can never grow.
See also the loop invariants blog post for more on how contracts support correctness reasoning.
Feature summary
| Feature | Description | Ticket |
|---|---|---|
|
Assert a condition at the start of each iteration of any loop type
(for-in, classic for, while, do-while). Multiple invariants can be stacked.
Violations throw |
|
Loop termination measure (loop variant). Takes a closure returning a
value or list of values that must strictly decrease each iteration while
remaining non-negative. Lists use lexicographic comparison.
Violations throw |
||
Frame condition declaring which fields a method may change.
Everything not listed is guaranteed unchanged.
@Pure is shorthand for |
||
Contracts in scripts |
|
Type Checking Extensions
Groovy’s type checking is extensible, allowing you to strengthen
type checking beyond what the standard checker provides.
Groovy 6 adds support for parameterized type checking extensions
(GROOVY-11908),
allowing extensions to accept configuration arguments
directly in the @TypeChecked
annotation string.
Several new type checking extensions take advantage of this capability.
The new checkers below all live in the groovy.typecheckers package.
NullChecker
The NullChecker extension
(GROOVY-11894)
validates code annotated with @Nullable, @NonNull, and
@MonotonicNonNull annotations, detecting null-related errors
at compile time. It recognises these annotations by simple name
from any package (JSpecify, JSR-305, JetBrains, SpotBugs,
Checker Framework, or your own):
@TypeChecked(extensions = 'groovy.typecheckers.NullChecker')
int safeLength(@Nullable String text) {
if (text != null) {
return text.length() // ok: null guard
}
return -1
}
assert safeLength('hello') == 5
assert safeLength(null) == -1
Without the null guard, dereferencing a @Nullable parameter
produces a compile-time error.
The checker also recognises safe navigation, early-exit patterns,
@NullCheck, and @MonotonicNonNull for lazy initialisation.
Strict mode: no annotations needed
Passing strict: true extends the checker with flow-sensitive analysis
that detects null issues even in completely unannotated code — no @Nullable, no @NonNull, no special types:
@TypeChecked(extensions = 'groovy.typecheckers.NullChecker(strict: true)')
static main(args) {
def x = null
x.toString() // compile error: 'x' may be null
def y = null
y = 'hello'
assert y.toString() == 'hello' // ok: reassigned non-null
}
The checker tracks nullability through assignments and control flow,
catching potential dereferences that would otherwise surface only at runtime.
This is also an example of parameterized type checking extensions in action — the strict: true argument is passed directly in the extension string.
See also the NullChecker blog post for a detailed walkthrough.
Feature summary
| Feature | Description | Ticket |
|---|---|---|
Parameterized extensions |
Type checking extensions can accept configuration arguments directly
in the |
|
|
Compile-time null safety. Validates |
|
|
Flow-sensitive null analysis without annotations. Tracks variables
assigned |
|
|
Recognises null-safety facts from |
|
Verifies |
||
Enforces functional purity at compile time. Verifies |
Designed for Human and AI Reasoning
A key design goal for Groovy 6 is making code easier to reason about — for both humans and AI. Several of the contract and type checking features described above work together to achieve this: @Modifies and @Pure declare what changes (and what doesn’t), @Requires and @Ensures declare what holds before and after, and type checking extensions like ModifiesChecker and PurityChecker verify these declarations at compile time. The combined effect is that each method becomes a self-contained specification — you can reason about what it does without reading its body.
Consider this annotated class, verified by both ModifiesChecker
and PurityChecker:
@TypeChecked(extensions = ['groovy.typecheckers.ModifiesChecker',
'groovy.typecheckers.PurityChecker'])
@Invariant({ balance >= 0 })
class Account {
BigDecimal balance = 0
List<String> log = []
@Requires({ amount > 0 })
@Ensures({ balance == old.balance + amount })
@Modifies({ [this.balance, this.log] })
void deposit(BigDecimal amount) {
balance += amount
log.add("deposit $amount")
}
@Requires({ amount > 0 && amount <= balance })
@Ensures({ balance == old.balance - amount })
@Modifies({ [this.balance, this.log] })
void withdraw(BigDecimal amount) {
balance -= amount
log.add("withdraw $amount")
}
@Pure
BigDecimal available() { balance }
}
When analyzing a sequence of calls:
account.deposit(100)
account.withdraw(30)
def bal = account.available()
With annotations, each call is a self-contained specification — 3 linear reasoning steps:
-
deposit(100):@Requiresmet (100 > 0),@Ensuresgivesbalance == old + 100,@Modifiesproves onlybalanceandlogchanged -
withdraw(30):@Requiresmet (30 > 0, 30 within balance),@Ensuresgivesbalance == 100 - 30 = 70,@Modifiesproveswithdrawdidn’t undo the deposit -
available():@Pureproves no side effects — just returnsbalance(70)
Without annotations, the analyzer must read every method body,
verify what each one modifies (2 fields × 3 calls = 6 "did this change?"
questions), re-verify earlier state after later calls, and check whether
available() has hidden side effects.
In general, this grows as O(fields × calls × call_depth) — which is where AI starts hallucinating or saying
"I’d need to see more context."
| What must be verified | With annotations | Without annotations |
|---|---|---|
Does |
No — |
Must read body + all callees |
Does |
No — |
Must read both method bodies |
Is |
Yes — |
Must read body, check for overrides |
What is |
Derive from |
Replay all mutations manually |
Can |
Check |
Must analyze all pairs for interference |
The type checkers provide the compile-time guarantee that these
annotations are truthful: ModifiesChecker verifies method bodies
only modify declared fields, and PurityChecker verifies @Pure
methods have no side effects. Without that guarantee, annotations
would be just comments — claims you’d still need to verify by
reading the code.
Module Import Declarations
Groovy 6 supports Java’s module import declarations (GROOVY-11896), giving scripts and classes a concise way to pull in every public type exported by a named module with a single statement:
import module java.sql
def conn = DriverManager.getConnection('jdbc:h2:mem:test')
def stmt = conn.createStatement()
An import module java.sql is equivalent to a star import
(import pkg.*) for every package the module exports, and also
covers packages reached via requires transitive. For example,
a single import module java.sql makes Connection and
DriverManager directly available, along with types like
javax.xml.transform.Source (from java.xml) and
java.util.logging.Logger (from java.logging).
Module imports work with system modules (JDK modules like
java.base, java.sql, java.desktop), JPMS modules from modular
JARs on the classpath, and automatic modules. Explicit single-type
imports take priority, so naming conflicts can be resolved by
adding a specific import:
import module java.desktop
import java.util.List
// The explicit single-type import wins over
// the module-expanded java.awt.List
assert List.name == 'java.util.List'
The module keyword is context-sensitive — it remains usable as
a variable, method, or class name. Wildcard
(import module java.base.*) and alias
(import module java.base as jb) forms are not supported.
See JEP 511 for the corresponding
Java language feature.
val Keyword for Final Declarations
Groovy 6 adds val as a contextual keyword for declaring final
variables and fields
(GROOVY-9308,
GEP-16).
val complements the existing var keyword: val declares an
immutable binding (equivalent to final def), while var continues
to declare a mutable one. The shape and naming will be familiar to
anyone who has seen Kotlin or Scala code declaring read-only (immutable) variables.
val name = 'Groovy' // equivalent to: final def name = 'Groovy'
val list = [1, 2, 3] // shallow finality — list contents may still mutate
list << 4 // OK
name = 'Other' // compile error: cannot assign to final variable
Like var, val is contextual — it remains usable as a variable
name, method name, map key, or property name. Existing code such as
def val = 1, obj.val, and [val: 42] continues to work
unchanged. val is rejected only where it would be ambiguous with a
type (for example, as a method return type or in a class val {}
declaration), mirroring the restrictions already in place for var.
When statically type-checking code, val carries the same type
inference rules as var:
val x = 42 // inferred as int
val s = 'hello' // inferred as String
val list = [1, 2, 3] // inferred as ArrayList<Integer>
A small number of pre-existing parser edge cases around var apply
equally to val — chiefly a field named val (or var) declared
immediately before a method or constructor, and val as Type cast
expressions. For codebases that need more time to migrate, a
-Dgroovy.val.enabled=false system property disables the keyword
entirely, lexing val as a plain identifier.
See GEP-16 for the full specification, including the migration flag and the complete list of edge cases.
Multi-assignment Destructuring
Groovy 6 extends def (…) multi-assignment with rest bindings and
map-style key destructuring
(GROOVY-11964).
Three idioms common in modern languages but previously unavailable in
Groovy are now supported. The extension is strictly additive — every program valid in Groovy 4 / 5 compiles with identical semantics — because each new shape uses an unparseable token sequence in the
existing grammar.
See GEP-20 for the full specification.
Tail rest binding
A trailing *ident captures the remaining elements:
def (h, *t) = [1, 2, 3, 4]
assert h == 1
assert t == [2, 3, 4]
def (a, b, c, *rest) = 'hello'
assert a == 'h' && b == 'e' && c == 'l'
assert rest == 'lo' // String slice — type tracks the RHS
The rest binding works against any RHS supporting either
getAt(IntRange) or iterator(). The compiler picks one of three
lowerings — a Stream rewrap that keeps lazy pipelines lazy, an
getAt(IntRange) slice (whose return type drives the rest binder’s
type), or an iterator fallback that supports unbounded sources without
materialising them:
// Lazy iterator — rest stays lazy
def naturals = (1..Integer.MAX_VALUE).iterator()
def (first, *more) = naturals
assert first == 1
assert more.next() == 2 && more.next() == 3
// Stream — pipeline preserved, sequential
def (header, *body) = Stream.of('# title', 'line 1', 'line 2')
assert header == '# title'
assert body.collect(Collectors.toList()) == ['line 1', 'line 2']
Rest binding in head and middle positions
*ident in non-tail position lowers via indexed access against a
sized, indexable RHS:
def (*front, last) = [1, 2, 3, 4]
assert front == [1, 2, 3]
assert last == 4
def (l, *middle, r) = [1, 2, 3, 4, 5]
assert l == 1 && r == 5
assert middle == [2, 3, 4]
def (a, b, *m, y, z) = 1..6
assert a == 1 && b == 2 && y == 5 && z == 6
assert m == [3, 4]
Map-style destructuring
key: ident pairs in the declarator list bind via property access
(map keys, JavaBean getters, or getProperty via the MOP):
def person = [name: 'Alice', age: 30, role: 'admin']
def (name: n, age: a) = person
assert n == 'Alice' && a == 30
// Type ascriptions pin or coerce binding types
def (name: String fullName, age: int years) = person
// Works on JavaBeans too
def (year: y, month: m) = Calendar.instance
Type ascriptions on rest binders
The rest binder may carry a type ascription, mirroring the existing positional form:
def (h, List<Integer> *t) = [1, 2, 3, 4]
def (c, String *cs) = 'hello'
def (l, List<Integer> *m, r) = [1, 2, 3, 4, 5]
def / var binder markers and modifier propagation
For symmetry with switch case patterns and bracket-form declarations,
def and var may appear before any binder; they are equivalent to
omitting a type. Modifiers on the outer declaration propagate to every
binder (including rest and map-style):
def (var a, var b) = [1, 2] // same as: def (a, b) = [1, 2]
def (def a, var b, int c) = [1, 2, 3] // mix and match
final (a, *t) = [1, 2, 3] // both `a` and `t` are final
final (name: n, age: a) = [name: 'A', age: 1] // both `n` and `a` are final
_ for unwanted slots
The "discard" convention (def (, y, m) = Calendar.instance) applies
uniformly across every new form. is a regular identifier
throughout — no wildcard semantics, no special parser node:
def (_, *t) = [1, 2, 3] // _ binds to the head
def (h, *_) = [1, 2, 3] // _ binds to the rest
def (*_, last) = [1, 2, 3] // _ binds to the front
def (l, *_, r) = [1, 2, 3, 4, 5] // _ binds to the middle
def (name: _, age: a) = [name: 'A', age: 30] // _ binds to the name slot
Compound Assignment Operator Overloading
Groovy 6 lets classes overload the compound assignment operators
(+=, -=, *=, …) independently from their base operators
(GROOVY-11970,
GEP-15).
Historically, x += y always desugared to x = x.plus(y) — creating a
new value and rebinding the variable. That forced mutable types into
inefficient create-and-reassign patterns and made compound assignment
unavailable on final fields and variables.
A class can now define a dedicated *Assign method (plusAssign,
minusAssign, multiplyAssign, and so on). When such a method is
resolved on the receiver, the operator mutates the receiver in place
instead of reassigning the LHS:
class Accumulator {
int total = 0
void plusAssign(int n) { total += n } // mutate in place
Accumulator plus(int n) { new Accumulator(total: total + n) } // create new
}
def acc = new Accumulator()
acc += 5 // calls plusAssign — no reassignment
assert acc.total == 5
Because no reassignment occurs when Assign is used, compound
assignment now works on final fields and variables under
@CompileStatic/@TypeChecked whenever a matching *Assign method
exists on the receiver type. For property LHS, the setter is *not
invoked. The expression value of x op= y is the (mutated) x, not the
return value of *Assign.
The change is strictly additive: if no *Assign method matches, the
legacy x = x.op(y) desugar still applies, so existing code keeps
working. Resolution is direct under static compilation and via the MOP
in dynamic code; *Assign methods are also discoverable as extension
methods or categories. Names can be remapped via
@OperatorRename (e.g.
@OperatorRename(plusAssign='addInPlace')).
The full set of compound assignment operators and their corresponding
*Assign methods (with op fallbacks) is:
| Operator | Assign method | Fallback method |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
See GEP-15 for the full specification.
Extension method additions and improvements
Groovy provides over 2000 extension methods to 150+ JDK classes to enhance JDK functionality, with new methods added in Groovy 6.
groupByMany — multi-key grouping
Several variants of groupByMany
(GROOVY-11808)
exist for grouping lists, arrays, and maps of items by multiple keys — similar to Eclipse Collections' groupByEach and a natural fit for
many-to-many relationships that SQL handles with GROUP BY.
The most common form takes a closure that maps each item to a list of keys:
var words = ['ant', 'bee', 'ape', 'cow', 'pig']
var vowels = 'aeiou'.toSet()
var vowelsOf = { String word -> word.toSet().intersect(vowels) }
assert words.groupByMany(s -> vowelsOf(s)) == [
a:['ant', 'ape'], e:['bee', 'ape'], i:['pig'], o:['cow']
]
For maps whose values are already lists, a no-args variant groups keys by their values:
var availability = [
'🍎': ['Spring'],
'🍌': ['Spring', 'Summer', 'Autumn', 'Winter'],
'🍇': ['Spring', 'Autumn'],
'🍒': ['Autumn'],
'🍑': ['Spring']
]
assert availability.groupByMany() == [
Winter: ['🍌'],
Autumn: ['🍌', '🍇', '🍒'],
Summer: ['🍌'],
Spring: ['🍎', '🍌', '🍇', '🍑']
]
A two-closure form also exists for transforming both keys and values. See the groupByMany blog post for more examples including Eclipse Collections interop.
Process handling
waitForResult replaces the manual stream/exit-code dance with a single call
(GROOVY-11901):
var result = 'echo Hello World'.execute().waitForResult()
assert result.output == 'Hello World\n'
assert result.exitValue == 0
// With timeout
var result = 'sleep 60'.execute().waitForResult(5, TimeUnit.SECONDS)
Asynchronous file I/O
The groovy-nio module adds async file operations on Path that return
CompletableFuture results
(GROOVY-11902).
These compose naturally with Groovy 6’s async/await:
import java.nio.file.Path
// Read two files concurrently
def a = Path.of('config.json').textAsync
def b = Path.of('data.csv').textAsync
def (config, data) = await(a, b)
Other new extension methods
| Method | Description | Ticket |
|---|---|---|
|
Check whether elements of an Iterable, Iterator, array, or Map
are in sorted order. Supports natural ordering, |
|
|
Named parameters for process configuration: |
|
|
Convert a String, String array, or List into a |
|
|
Create native OS pipelines from a list of commands via
|
|
|
Register a closure to execute asynchronously when a process terminates.
|
|
|
Asynchronous file reading on |
|
|
Asynchronous file writing on |
|
|
Lazy |
|
|
Find the first occurrence of a regex within a |
|
|
Return a list of all matches of a regex within a |
Selectively Disabling Extension Methods
The groovy.extension.disable system property has been enhanced
(GROOVY-11892),
to allow finer-grained control over which
Groovy extension methods are disabled. Previously, setting
-Dgroovy.extension.disable=groupBy would disable all overloads
of groupBy. Now, specific overloads can be targeted by
receiver type or full parameter signature:
| Syntax | Effect |
|---|---|
|
Disables all |
|
Disables only the overload for |
|
Disables all overloads of both methods |
Type names can be simple (Set) or fully qualified (java.util.Set).
This is particularly useful when integrating with libraries like
Eclipse Collections that define
methods with the same name as Groovy’s extension methods but return
different types. For example, Groovy’s groupBy returns lists or maps
from the standard collections library, but Eclipse Collections' groupBy returns a Multimap.
By disabling the Groovy overload only for Lists,
we can still use Groovy’s groupBy on java.util.Map instances.
// disable groupBy only for Lists
// (well, all iterables but only for the Closure variant)
// -Dgroovy.extension.disable=groupBy(Iterable,Closure)
var fruits = Lists.mutable.of('🍎', '🍌', '🍎', '🍇', '🍌')
// Eclipse Collections groupBy → returns a Multimap
assert fruits.groupBy { it } ==
Multimaps.mutable.list.empty()
.withKeyMultiValues('🍎', '🍎', '🍎')
.withKeyMultiValues('🍌', '🍌', '🍌')
.withKeyMultiValues('🍇', '🍇')
// Groovy's groupBy still works on Maps
def result = [a:1,b:2,c:3,d:4].groupBy { it.value % 2 }
assert result == [0:[b:2, d:4], 1:[a:1, c:3]]
Customisable Object Display with groovyToString
Groovy 6 introduces a groovyToString() protocol
(GROOVY-11893)
that lets classes control how their instances appear in string
interpolation, println, collection formatting, and other display contexts.
When a class defines a groovyToString() method returning String,
Groovy uses it instead of toString() for display purposes:
class Foo {
String toString() { 'some foo' }
String groovyToString() { 'some bar' }
}
assert "${new Foo()}" == 'some bar'
assert [foo: new Foo()].toString() == '[foo:some bar]'
Groovy also provides built-in groovyToString extension methods for
collections, maps, ranges, and primitive arrays, giving them their
familiar Groovy formatting (e.g. [1, 2, 3] for int[] rather than
Java’s [I@hashcode). These can be selectively disabled using the
groovy.extension.disable system property if needed.
GINQ Enhancements
groupby … into
GINQ’s groupby clause now supports an into keyword
(GROOVY-11915)
that binds each group to a named variable with aggregate access:
GQ {
from n in [1, 11, 111, 8, 80]
groupby (n % 2 == 0 ? 'even' : 'odd') into g
select g.key, g.count() as count, g.sum(n -> n) as sum, g.toList() as numbers
}
+------+-------+-----+--------------+ | key | count | sum | numbers | +------+-------+-----+--------------+ | even | 2 | 88 | [8, 80] | | odd | 3 | 123 | [1, 11, 111] | +------+-------+-----+--------------+
Set operators
SQL-style set operators
(GROOVY-11919)
for combining query results: union, intersect, minus, and unionall:
def java = ['Alice', 'Bob', 'Carol']
def groovy = ['Bob', 'Carol', 'Dave']
assert GQL {
from n in java select n
union
from n in groovy select n
} == ['Alice', 'Bob', 'Carol', 'Dave']
See the GINQ user guide for the full set of clauses and operators.
CSV Module (incubating)
Groovy 6 adds a new groovy-csv module
(GROOVY-11923)
for reading and writing CSV (RFC 4180) data.
Classes live in the groovy.csv package.
Reading CSV
CsvSlurper parses CSV text into a list of maps, keyed by column headers:
def csv = new CsvSlurper().parseText('name,age\nAlice,30\nBob,25')
assert csv.size() == 2
assert csv[0].name == 'Alice'
assert csv[0].age == '30'
The separator and quote characters can be customised via fluent setters,
and quoted fields follow RFC 4180 rules (embedded commas, newlines, and
doubled quotes). Set useHeader = false to treat the first row as data
and key rows by auto-generated column names. Reader/InputStream/File/Path
overloads of parse are provided alongside parseText.
Writing CSV
CsvBuilder converts collections of maps to CSV:
def data = [
[name: 'Alice', age: 30],
[name: 'Bob', age: 25]
]
def csv = CsvBuilder.toCsv(data)
assert csv.contains('name,age')
assert csv.contains('Alice,30')
Typed parsing and writing
When Jackson is on the classpath, CsvSlurper can parse CSV directly
into typed objects, and CsvBuilder can write typed objects to CSV.
This is particularly useful for CSV since all values are strings — Jackson handles conversion to numeric, date, and other types automatically:
class Sale {
String customer
BigDecimal amount
}
def sales = new CsvSlurper().parseAs(Sale, 'customer,amount\nAcme,1500.00\nGlobex,250.50')
assert sales[0].customer == 'Acme'
assert sales[0].amount == 1500.00
See the Processing CSV user guide for the full API and configuration options.
Typed Parsing and Writing Across Format Modules
Groovy 6 brings typed parsing support across all data format modules, giving a consistent way to convert structured data into typed objects. Given a target class:
class ServerConfig { String host; int port; boolean debug }
Each format can parse directly into it:
// JSON — as coercion (no extra deps)
def config = new JsonSlurper().parseText(json) as ServerConfig
// TOML — Jackson-backed parseTextAs
def config = new TomlSlurper().parseTextAs(ServerConfig, toml)
// XML — Jackson-backed parseTextAs
def config = new XmlParser().parseTextAs(ServerConfig, xml)
| Format | Typed Parsing | Typed Writing |
|---|---|---|
JSON |
JsonSlurper + |
JsonOutput |
CSV (GROOVY-11923) |
CsvSlurper |
CsvBuilder |
TOML (GROOVY-11925) |
TomlSlurper |
TomlBuilder |
YAML (GROOVY-11926) |
YamlSlurper |
YamlBuilder |
XML (GROOVY-11927) |
XmlParser |
— |
|
Note
|
For CSV, TOML, YAML, and XML, the parseTextAs/parseAs methods use
Jackson databinding and support @JsonProperty, @JsonFormat, etc.
For simple cases, Groovy’s as coercion works without Jackson.
For XML, jackson-dataformat-xml can be used directly for full
Jackson XML annotation support.
|
Markdown Module (incubating)
Groovy 6 adds a new optional groovy-markdown module
(GROOVY-11940)
for parsing CommonMark Markdown into a
navigable document model. Classes live in the groovy.markdown
package. A common motivating use case is extracting structured
pieces from LLM output — code blocks by language, sections by
heading, links, and tables — but the module is useful anywhere
Markdown is processed programmatically.
MarkdownSlurper
MarkdownSlurper parses Markdown
text into a MarkdownDocument
backed by nested lists and maps. Each node is a Map with a type
key plus type-specific fields:
def doc = new MarkdownSlurper().parseText('# Hello World')
def h = doc.headings[0]
assert h.level == 1
assert h.text == 'Hello World'
The document exposes convenience properties — headings,
codeBlocks, links, tables — that recursively walk the tree,
so nodes nested inside list items or block quotes are still found.
A text property gives a plain-text projection of the whole
document with formatting markers stripped.
Extracting code blocks
Pulling fenced code blocks by language is a common pattern when consuming LLM output:
def doc = new MarkdownSlurper().parseText(md)
def groovySnippets = doc.codeBlocks.findAll { it.lang == 'groovy' }*.text
Sections
section(headingText) returns the nodes between a given heading
and the next heading of equal or higher level — handy for parsing
structured agent replies:
def doc = new MarkdownSlurper().parseText(md)
def next = doc.section('Next Steps')
assert next[0].type == 'list'
assert next[0].items*.text == ['Item one', 'Item two']
Tables (optional)
GFM-style tables are supported when the
org.commonmark:commonmark-ext-gfm-tables jar is on the classpath.
Call enableTables(true) on the slurper and each row comes back as
a Map keyed by header:
def doc = new MarkdownSlurper().enableTables(true).parseText(md)
def rows = doc.tables[0].rows
assert rows[0].name == 'Alice'
assert rows[1].age == '25'
Supported node types include heading, paragraph, code_block,
list/list_item, block_quote, link, image, text,
inline_code, emphasis/strong, html_block/html_inline,
thematic_break, line breaks, and (with the GFM extension) table.
See the Processing Markdown user guide for the full node schema and additional examples.
Grape: Dual Engine Support (incubating)
Groovy 6 introduces a major evolution of the Grape dependency management system (GROOVY-11871) by adding a second built-in engine alongside the existing Apache Ivy backend. Both engines expose the same @Grab family of annotations and the Grape facade API, so most existing scripts work unchanged.
Engine comparison
| Aspect | GrapeIvy (default) | GrapeMaven (new) |
|---|---|---|
Backend |
||
Engine class |
|
|
Local cache |
|
|
Configuration file |
|
None — use |
|
Ivy configurations; lists supported (e.g. |
Single Maven scope only |
|
Honoured (cache-only resolution) |
Not honoured |
Version wildcard |
Resolves to Ivy’s |
Resolves to Maven’s |
Both engines respect the grape.root system property for relocating the cache root.
Selecting the engine
When both engines are on the classpath, GrapeIvy is selected by default. To switch to GrapeMaven for a specific invocation:
groovy -Dgroovy.grape.impl=groovy.grape.maven.GrapeMaven yourscript.groovy
Set the same property in JAVA_OPTS for a global default.
Custom engines via Java SPI
Custom GrapeEngine implementations are
discovered via the standard java.util.ServiceLoader mechanism by
including a jar with a META-INF/services/groovy.grape.GrapeEngine
entry naming the implementation class. See the user guide for the
full registration protocol.
Migration
Most existing @Grab scripts work unchanged with both engines. The
notable differences when moving from GrapeIvy to GrapeMaven:
-
Multi-value
conf:parameters (e.g.conf:['default','optional']) are not supported — GrapeMaven uses a single Maven scope. -
@GrabConfig(autoDownload=false)is not honoured — point@GrabResolverat a local-only repository for similar semantics. -
~/.groovy/grapeConfig.xmlsettings are GrapeIvy-only — register custom repositories via@GrabResolverorGrape.addResolverinstead.
Switching from GrapeMaven back to GrapeIvy is generally straightforward since GrapeIvy’s defaults are more permissive.
See the Grape user guide for the full per-engine specification, custom-engine registration steps, per-engine logging configuration, and detailed migration walkthrough.
Platform Logging
Groovy 6 replaces direct System.err output with the JDK’s
Platform Logging API (java.lang.System.Logger)
for internal errors and warnings
(GROOVY-11886).
This means Groovy’s diagnostic messages can now be controlled
through standard JVM logging configuration.
By default, messages still appear on the console via java.util.logging,
but users can plug in any logging framework (SLF4J, Log4j2, etc.)
by providing a System.LoggerFinder implementation on the classpath.
Configuring logging
Groovy’s command-line tools resolve logging configuration in the following order (first match wins):
-
A file specified via
-Djava.util.logging.config.file=… -
A user configuration at
~/.groovy/logging.properties(auto-discovered by Groovy at startup) -
The JDK default at
$JAVA_HOME/conf/logging.properties
For example, to enable verbose Grape logging, create
~/.groovy/logging.properties with:
handlers = java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level = ALL
groovy.grape.Grape.level = FINE
Loggers are organized by module (Core, Grape, Ant, Console, GroovyDoc, JSON, Servlet, Swing, Testing) — see the logging guide for the full list of logger names and their levels.
JUnit 6 Support
Groovy 6 updates its testing support to include JUnit 6 (GROOVY-11788). JUnit 6 (Jupiter 6.x) is the latest evolution of JUnit, building upon the JUnit 5 platform.
Using JUnit 6 with Groovy
For most users, no special Groovy module is needed — simply add the standard JUnit Jupiter dependencies to your Gradle or Maven project and write tests in Groovy as usual:
// build.gradle
testImplementation 'org.junit.jupiter:junit-jupiter:6.0.3'
The groovy-test-junit6 module
The new groovy-test-junit6 module
provides additional capabilities for users who need them:
-
Running individual test scripts — execute a Groovy test file directly as a script without a full build tool setup
-
Conditional test execution annotations — scripting support for JUnit Jupiter’s conditional execution annotations (GROOVY-11887), allowing conditions to be expressed as Groovy scripts
-
Convenient dependency management — a single dependency that transitively pulls in compatible JUnit 6 libraries
Similarly, the existing groovy-test-junit5 module continues
to provide the same capabilities for JUnit 5 users.
GroovyConsole: Script Arguments
GroovyConsole now supports setting script arguments directly from the UI
(GROOVY-11895).
Previously, users had to manually set args within their script
as a workaround. A new Set Script Arguments option in the Script menu
lets you specify space-separated arguments that are passed to the script.
Theming Improvements
To match recent theming improvements to the Groovy website — which gained light, dark, and "follow system" options earlier in the year — Groovy 6 brings consistent light/dark/custom theming to its documentation tooling and to GroovyConsole.
Theming is more than visual polish or matching personal preference: it is also an accessibility improvement. Readers with light sensitivity, low-vision users running high-contrast palettes, and anyone working in a low-light environment all benefit from a tool surface that respects the system colour scheme rather than forcing one on them.
GroovyDoc
GroovyDoc gained a -theme flag controlling the output palette
strategy
(GROOVY-11947):
-
auto(default) — follow the reader’s OSprefers-color-scheme. Both palettes are emitted; the browser picks at view time. -
light/dark— lock the output to a single palette regardless of OS preference.
The default stylesheet has been refactored around semantic CSS
custom properties (--fg, --bg, --link, --bg-panel, etc.), so
author stylesheets supplied via --add-stylesheet can reference the
same tokens and become theme-aware automatically — the same hook
that enables fully custom palettes. When Prism.js syntax
highlighting is enabled, the highlight theme swaps in sync with the
page palette
(GROOVY-11946).



docgenerator (Groovy user guide)
The same theming model has been extended to docgenerator, the
tool that produces the Groovy user guide and other
distribution-bundled documentation. Output supports light, dark,
and custom themes, following the OS preference by default and
sharing the GroovyDoc CSS-custom-property approach so that custom
stylesheets remain palette-aware.


GroovyConsole
GroovyConsole now offers light, dark, follow-system, and custom theme options, applied across the editor pane, output pane, and surrounding chrome. The active choice is persisted between sessions.



GroovyDoc Enhancements
Groovy 6 brings GroovyDoc substantially closer to modern Javadoc
feature parity and adds a few enhancements of its own. The two
headline items are support for JEP 467 Markdown doc comments and
JEP 413 {@snippet} code snippets, both working in .groovy and
.java sources.
Markdown doc comments (JEP 467)
A run of /// line comments can now be used as a doc comment whose
body is CommonMark Markdown
(GROOVY-11542):
/// # Greet
///
/// Returns a friendly greeting. Inline tags still work:
/// {@link String} and {@code greet}.
///
/// ```groovy
/// assert greeter.greet('world') == 'Hello, world'
/// ```
///
/// @param name the subject to greet
/// @return the greeting
String greet(String name) { "Hello, $name" }
Headings, bullets, fenced code blocks, emphasis, and inline code all
render as you would expect. Headings are shifted down two levels
(so # becomes <h3>), fitting under each page’s existing title
structure. Inline Javadoc tags continue to work inside Markdown
bodies; tag-like text inside fenced code blocks stays literal.
Traditional /** … */ Javadoc blocks still work and can coexist
with /// comments in the same source file.
See JEP 467 and CommonMark for the full syntax and semantics.
Code snippets via {@snippet}
The JEP 413 {@snippet} tag is now supported
(GROOVY-11938)
in three forms. Inline body:
/**
* {@snippet lang="groovy" :
* def greet(name) { "Hello, $name" }
* greet('world')
* }
*/
External reference to a file in the package’s snippet-files/
directory (a region can also be selected via region="name"):
/** {@snippet file="GreetExample.groovy"} */
Markup comments inside snippet bodies let authors highlight, replace, or link to portions of the code without editing the source itself:
/**
* {@snippet lang="groovy" :
* def greet(name) { "Hello, $name" } // @highlight substring="greet"
* // @link substring="Greeter" target="Greeter"
* Greeter.instance.greet('world')
* }
*/
@highlight, @replace, and @link all support substring=,
regex=, and region= attributes. See
JEP 413 for the full markup
reference.
Two author-facing conveniences apply to the external form: the
language class is inferred from the file’s extension when no
explicit lang="…" is given, and a leading license or copyright
header in the referenced file is stripped from the rendered output
(opt out with keepHeader=true). See the
dochome:groovydoc.html[Groovydoc
documentation] for the full list of recognised extensions and the
header-detection heuristic.
Syntax highlighting and dark mode
Two new generator flags make the rendered output immediately more modern:
groovydoc -syntaxHighlighter prism -theme auto ...
-syntaxHighlighter=prism bundles
Prism.js for client-side syntax highlighting
of {@snippet} blocks and Markdown fenced code. Groovy, Java, XML,
JSON, YAML, TOML, SQL, CSV, Markdown, JavaScript, and regex are
supported out of the box.
-theme controls the palette strategy
(GROOVY-11947):
-
auto(default) — follow the reader’s OSprefers-color-scheme. Light and dark palettes are both emitted; the browser picks at view time. Syntax highlighting swaps themes in sync (GROOVY-11946). -
light/dark— lock the output to one palette regardless of OS preference.
Under the hood, the default stylesheet has been refactored around
semantic CSS custom properties (--fg, --bg, --link,
--bg-panel, etc.). Author stylesheets supplied via
--add-stylesheet can reference the same tokens and become theme-aware
automatically.
Highlighting legacy <pre> blocks
The new -preLanguage <lang> option (CLI) / preLanguage="lang"
attribute (Ant task) sets a default Prism language for unattributed
<pre> blocks in doc comments, so legacy code samples get
highlighted without source edits
(GROOVY-11950).
See the
dochome:groovydoc.html[Groovydoc
documentation] for the rewrite rules and the per-block opt-out.
Feature summary
Smaller improvements that collectively close the long-standing gap with modern Javadoc:
| Feature | Description |
|---|---|
Class hierarchy tree pages |
|
Script documentation |
Top-level scripts now get documented; leading |
|
Members annotated with |
|
Inlines compile-time constants. Supports |
|
Pulls in the parent class’s/interface’s corresponding documentation (GROOVY-3782) |
|
Recognised with proper heading labels (e.g. "Implementation Requirements:" rather than raw tag names) (GROOVY-11945) |
|
Annotation references are only emitted when the annotation type
itself is |
|
Per-package directories are copied verbatim to the output for sibling images, diagrams, and external snippet sources (GROOVY-5986) |
|
Add a custom stylesheet alongside the default (contrast with
|
Optional-page toggles |
|
Ant |
JPMS-style module segment in external-link URLs for JDK 9+ Javadoc layouts (GROOVY-11682) |
Non-zero exit on errors |
CLI returns a non-zero exit code when source files fail to parse, so CI jobs fail rather than silently succeed (GROOVY-9057) |
Miscellaneous fixes |
No spurious |
Build tool support
The new options above are exposed through the GroovyDoc CLI
(groovydoc) and the Ant task. Gradle’s built-in groovydoc
task currently lags behind both surfaces and does not yet expose
every new flag.
Until that gap closes, we recommend either driving GroovyDoc via the Ant task from Gradle, or replicating the approach used by the Apache Groovy and Grails builds themselves — both projects wrap GroovyDoc in their own plugin/build glue to access the full feature set.
Improved Annotation Validation
Groovy 6 closes gaps in annotation target validation (GROOVY-11884). Previously, annotations could be placed on import statements and loop statements without validation — for example:
@Deprecated import java.lang.String
would compile without error even though @Deprecated does not target imports.
The compiler now enforces that only annotations explicitly declaring Groovy-specific targets are permitted in these positions. A new @ExtendedTarget meta-annotation with an ExtendedElementType enum defines two Groovy-only targets:
-
IMPORT— for annotations valid on import statements (e.g. @Grab and related annotations, @Newify, @BaseScript) -
LOOP— for annotations valid on loop statements (e.g. @Invariant, @Decreases, @Parallel)
Annotations without the appropriate @ExtendedTarget declaration
are now flagged as compile errors when applied to these constructs.
This is a breaking change for code that
previously relied on the lenient behavior.
Other Core API Changes
-
ASTTransformationCustomizernow supports annotations whose@GroovyASTTransformationClasslists multiple transforms (e.g.@Sealed,@RecordBase) and@AnnotationCollectoraliases (e.g.@AutoExternalize), via a newforAnnotation(…)static factory that returns one customizer per resolved transform class (GROOVY-11973). For example:config.addCompilationCustomizers(*ASTTransformationCustomizer.forAnnotation(Sealed)).
Other Module Changes
-
groovy-xml:
XmlParserandXmlSlurpernow support named parameter construction for all parser options includingvalidating,namespaceAware, andallowDocTypeDeclaration(GROOVY-7633). For example:new XmlParser(namespaceAware: false, trimWhitespace: true). -
groovy-xml:
XmlUtil.serializenow accepts aSerializeOptionsparameter for controlling encoding, indentation, and DOCTYPE handling (GROOVY-7571). For example:XmlUtil.serialize(node, new SerializeOptions(encoding: 'ISO-8859-1', indent: 4)). -
groovy-xml: New StAX streaming helpers and secure-by-default JAXP factory accessors.
groovy.xml.FactorySupportis extended to cover all six JAXP factory types (addsXMLInputFactory,TransformerFactory,SchemaFactoryandXPathFactoryalongside the existingDocumentBuilderFactoryandSAXParserFactory), with explicitallowDocTypeDeclaration/allowExternalResourcesoverloads where appropriate.XmlUtil.events(reader)returns aStream<XMLEvent>over a hardenedXMLInputFactory;XmlUtil.streamElements(reader, [namespaceURI,] localName)pulls each matching subtree as a small DOMNode, suitable for streaming multi-gigabyte feeds without loading them into memory.SerializeOptionsadds anallowExternalResourcesfield;DOMBuilder.newInstanceadds a 3-arg overload takingallowDocTypeDeclaration. A new XML security chapter in the XML user guide documents the secure-by-default contract end-to-end. (GROOVY-11979). -
groovy-sql: The
DataSetclass now correctly handles non-literal expressions in queries (GROOVY-5373). -
groovy-sql: New
Sql.inListhelper expands a collection into the parameter list of a SQLINclause, avoiding manual string concatenation (GROOVY-5436). -
groovy-sql: Added Map-based named-parameter overloads for
call,callWithRows, andcallWithAllRows, matching the existing named-parameter support oneachRow,rows, etc. (GROOVY-11936). -
groovy-ant: The
<groovy>Ant task now inherits the surrounding project’s Ant properties when running in forked mode, matching the behaviour of non-forked execution (GROOVY-6908). -
groovy-jmx: Removed dead IIOP support and refreshed documentation (GROOVY-11921).
-
groovy-servlet: Jakarta EE 11 compatibility (GROOVY-11922).
Breaking changes
Removal of Security Manager support
Java’s Security Manager has been deprecated for removal by JEP 411, which argues that it is rarely used to secure modern applications and that security is better achieved through other mechanisms such as containers and operating system security.
Groovy 6 removes its use of AccessController.doPrivileged calls
and related Security Manager infrastructure
(GROOVY-10581).
Code that relied on Groovy’s Security Manager integration
should adopt alternative security mechanisms.
Groovy 5 still includes such support on JDK versions that support it.
Other changes
-
valis now a contextual keyword for declaring final variables (seevalKeyword for Final Declarations). A few pre-existing edge cases around the siblingvarkeyword now apply tovalas well — chiefly a field namedvaldeclared immediately before a method or constructor, andval as Typecast expressions. The-Dgroovy.val.enabled=falsesystem property reverts to the prior behavior as a porting aid. (GROOVY-9308) -
Annotation target validation is now enforced for import and loop statements (see Improved Annotation Validation). (GROOVY-11884)
-
The inner class
methodMissingandpropertyMissingprotocol was redesigned. Some scenarios that previously allowed access to an outer class’s members through an inner class instance (e.g. accessing outer fields via an object expression, outer classinvokeMethod/MOP overloads from anonymous inner classes) may no longer work. (GROOVY-11853) -
When multiple
set(String, …)method overloads exist, Groovy now selects the best-matching overload based on the value type rather than always using the same method. This fixes incorrectGroovyCastExceptionerrors at runtime but may change which setter is invoked if your code relied on the previous behavior. (GROOVY-11829) -
TomlSlurperandYamlSlurperno longer prematurely closeReaderandInputStreamarguments passed to parsing methods. Callers are now responsible for closing resources they create, following standard conventions. (GROOVY-11925, GROOVY-11926) -
XmlParser.parse(File)andXmlParser.parse(Path)now properly close the underlyingInputStreamafter parsing. Previously, the stream was not closed, which could cause file descriptor leaks. (GROOVY-11927) -
XmlParser.setNamespaceAware(boolean)now throwsIllegalStateExceptionif called after parsing has started. Previously, the setter silently updated the field but had no effect since the SAX parser was already configured. (GROOVY-7633) -
groovy-xml hardening: three default-behaviour changes to behind-the-scenes XML factory creation paths. The front-line parsers (
XmlParser,XmlSlurper, the staticDOMBuilder.parse(…)overloads,XmlUtil.newSAXParser) were already secure-by-default and are unaffected. (1)XmlUtil.serializenow blocks external<xsl:import>/<xsl:include>and external DTD references in the underlyingTransformerFactory. Affects callers passing XSLT documents with external resource references throughserialize; opt back in viaSerializeOptions.allowExternalResources=true. The common case of pretty-printing already-parsed nodes or DOM trees is unaffected. (2)FactorySupport.createDocumentBuilderFactory()andcreateSaxParserFactory()zero-arg variants now return hardened factories instead of bare JDK factories. Direct callers that were parsing DOCTYPE-bearing input through them should switch to the new(true)overload. (3) Mostly theoretical:DOMBuilder.newInstance()andnewInstance(validating, namespaceAware)now return a builder backed by a hardened factory. The DSL-build path doesn’t parse external input andparseTextalready routed through the hardened staticparse, so this only bites code that reaches intodomBuilder.documentBuilderand parses DOCTYPE-bearing XML directly. NewnewInstance(validating, namespaceAware, allowDocTypeDeclaration)overload provides a relax knob. (GROOVY-11981) -
The last references to
javax.swing.JApplethave been removed fromgroovy-swing(theSwingBuilderfactory registration) and from the Groovy Console.JAppletwas deprecated for removal since Java 9 and has been removed in JDK 26, so these references would otherwise causeNoClassDefFoundErroratSwingBuilderinitialisation on JDK 26+. Code that explicitly used the applet-related factories should migrate to standardJFrame/JWindowalternatives. (GROOVY-11912)
Under exploration (before GA release)
-
Further performance improvements
-
Further improvements to the language specification documentation
JDK requirements
Groovy 6 requires JDK17+ to build and JDK17 is the minimum version of the JRE that we support. Groovy 6 has been tested on JDK versions 17 through 26.
More information
You can browse all the tickets closed for Groovy 6.0 in JIRA.