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.

Note
WARNING: Material on this page is still under development! We are currently working on alpha versions of Groovy 6.0 with a goal of gathering feedback on the language changes from our community. In addition, early versions assist other projects and tool vendors within the Groovy ecosystem to begin assessing the impact of moving to/supporting Groovy 6.0. Caution should be exercised if using new features as the details may change before final release. Some features described here as "incubating" may become stable before 6.0.0 final is released, others are expected to remain incubating for version 6. We don’t recommend using alpha versions or incubating features for production systems.

Highlights

Native Async/Await (incubating) (see Native Async/Await (incubating))

  • Sequential-style concurrent code — no callbacks or CompletableFuture chains.

  • 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 via AsyncScope.

Integrated Concurrency Toolkit (incubating) (see Integrated Concurrency and Parallel Processing (incubating))

  • Unified groovy.concurrent package: agents, actors, dataflow variables, channels, and parallel collections.

  • @ActiveObject adds actor semantics to ordinary classes — no message protocols to hand-write.

  • Parallel Collection methods (collectParallel, findAllParallel, eachParallel, …​) for CPU-bound work.

  • Same APIs available to Java, Kotlin and other JVM languages via the standalone groovy-concurrent-java module (see Java-only module: groovy-concurrent-java).

New HTTP Client Module (incubating) (see HttpBuilder: HTTP Client Module (incubating))

  • groovy-http-builder offers both an imperative DSL and a declarative @HttpBuilderClient interface.

  • Built on the JDK’s java.net.http.HttpClient with native async support.

  • Auto-parsed JSON, XML and HTML responses; typed return objects driven by interface signatures.

New Language Features

Designed for Human and AI Reasoning (see Designed for Human and AI Reasoning)

  • New NullChecker type checker with an optional flow-sensitive strict mode requiring no annotations (see Type Checking Extensions).

  • @Modifies frame conditions and @Pure purity declarations, verified at compile time by ModifiesChecker and PurityChecker.

  • 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)

Extension Method Additions (see Extension method additions and improvements)

  • New methods including groupByMany, waitForResult, findGroups/findAllGroups, isSorted, and lazy grepping.

  • Asynchronous file I/O on Path (textAsync, bytesAsync, writeAsync) returning CompletableFuture — composes with await.

  • Streamlined process handling: pipeline, onExit, toProcessBuilder, named-parameter execute(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 …​ into binds 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

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

groovy-concurrent-java

Standalone Java library exposing the groovy.concurrent toolkit (structured concurrency, actors, agents, dataflow, pools) without the Groovy runtime — see Java-only module: groovy-concurrent-java

groovy-csv

CSV parsing and writing via Jackson CSV

groovy-grape-ivy

@Grab engine backed by Apache Ivy — previously in core, now its own module

groovy-grape-maven

New @Grab engine backed by Maven Resolver

groovy-http-builder

Imperative DSL and declarative annotation-driven client over JDK java.net.http.HttpClient

groovy-markdown

CommonMark Markdown parser

groovy-reactor

AwaitableAdapter SPI for Project Reactor (Mono/Flux) — enables await and for await over reactive types

groovy-rxjava

AwaitableAdapter SPI for RxJava 3 (Single/Maybe/Observable/Flowable/Completable)

groovy-test-junit6

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

async { } / await

Start background tasks; collect results in sequential style

Virtual threads

Automatic on JDK 21+; cached thread pool fallback on JDK 17—​20

Awaitable.all(), multi-arg await(a, b, c)

Wait for all tasks to complete

Awaitable.any()

Race — first to complete wins

Awaitable.first()

First success wins (ignores individual failures)

Awaitable.allSettled()

Wait for all; inspect each outcome individually

yield return

Lazy generators with back-pressure

AsyncChannel

Buffered and unbuffered Go-style channels

for await

Iterate over async sources (generators, channels, reactive streams)

defer

LIFO cleanup actions, runs on scope exit regardless of success/failure

AsyncScope

Structured concurrency — child lifetime bounded by scope

Timeouts

Awaitable.timeout(duration) and Awaitable.timeout(duration, fallback)

Awaitable.delay()

Non-blocking pause

CompletableFuture / Future interop

await works directly with JDK async types

Framework adapters (SPI)

groovy-reactor (Mono/Flux), groovy-rxjava (Single/Observable)

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

Agent

Thread-safe mutable value updated via serialised functions; exposes a Flow.Publisher of state changes via changes().

GROOVY-11952, GROOVY-11953

Actor.reactor / Actor.stateful

Stateless and stateful actors with Awaitable-based reply via sendAndGet.

GROOVY-11952

@ActiveObject / @ActiveMethod

AST-driven actor semantics with class syntax. blocking = false returns an Awaitable.

GROOVY-11952

DataflowVariable, Dataflows

Single-assignment variables; Dataflows auto-creates on property access. Implements Awaitable and supports then chaining.

GROOVY-11952

AsyncChannel.filter / map / merge / split / tap

Composable channel pipelines.

GROOVY-11952

BroadcastChannel, ChannelSelect

One-to-many broadcast (with asPublisher() for Reactive Streams); Go-style multi-channel select.

GROOVY-11952, GROOVY-11953

Parallel collection methods

collectParallel, findAllParallel, eachParallel, sumParallel, injectParallel, groupByParallel, etc., on Collection.

GROOVY-11952

Pool and ParallelScope

Pool.cpu(), Pool.fixed(n), Pool.io(), Pool.virtual() pool factories; ParallelScope.withPool { } binds a pool for a block.

GROOVY-11952

groovy-concurrent-java

Standalone Java module exposing the same concurrent APIs without the Groovy runtime.

GROOVY-11952

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: @HttpBuilderClientTYPE; @Get/@Post/@Put/@Delete/@Patch/@Form/@TimeoutMETHOD; @Body/@BodyText/@QueryPARAMETER; @HeaderTYPE, 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

All (@Get, @Post, @Put, @Delete, @Patch)

JSON body / response

json() / result.json

@Body / return-type driven

Form-encoded body

form()

@Form

Plain text body

text()

@BodyText

XML / HTML response

result.xml / result.html

GPathResult / jsoup Document return type

Typed response objects

Manual (result.json as User)

Automatic (return type driven)

Query parameters

query()

Implied from parameter name (or @Query)

Path parameters

Manual

Auto-mapped via {name} placeholders

Headers

header() in config/request

@Header on interface/method

Async

getAsync(), postAsync(), etc.

CompletableFuture<T> return type

Timeouts (connect / request)

Config DSL

connectTimeout, requestTimeout on @HttpBuilderClient

Per-method timeout

Per-request timeout()

@Timeout(seconds)

Redirect following

Config DSL

followRedirects on @HttpBuilderClient

Error handling

Manual (check result.status())

Auto-throw; custom exception via throws clause

JDK client access (auth, SSL, proxy)

clientConfig { builder → …​ }

create { clientConfig { …​ } }

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 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

@Invariant on loops

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 LoopInvariantViolation. Targets: TYPE (existing) plus extended LOOP and IMPORT (new in 6.0).

GROOVY-11878

@Decreases

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 LoopVariantViolation. Targets: TYPE plus extended LOOP.

GROOVY-11890

@Modifies

Frame condition declaring which fields a method may change. Everything not listed is guaranteed unchanged. @Pure is shorthand for @Modifies({ [] }). Targets: CONSTRUCTOR, METHOD (both @Modifies and @Pure).

GROOVY-11909

Contracts in scripts

@Requires, @Ensures on script methods; @Invariant on import statements as a class-level invariant for the script.

GROOVY-11885

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 @TypeChecked annotation string, e.g. NullChecker(strict: true).

GROOVY-11908

NullChecker

Compile-time null safety. Validates @Nullable, @NonNull, and @MonotonicNonNull annotations. Recognises null guards, safe navigation, early exits, and @NullCheck.

GROOVY-11894

NullChecker(strict: true)

Flow-sensitive null analysis without annotations. Tracks variables assigned null, left uninitialized, or resulting from expressions with a null branch.

GROOVY-11894

NullChecker + @Requires/@Ensures

Recognises null-safety facts from groovy-contracts: top-level x != null conjuncts in @Requires mark parameters as implicit @NonNull, and result != null in @Ensures marks the return as implicit @NonNull.

GROOVY-11894

ModifiesChecker

Verifies @Modifies frame conditions at compile time. Checks that methods only assign to declared fields and that @Pure methods make no field assignments.

GROOVY-11910

PurityChecker

Enforces functional purity at compile time. Verifies @Pure methods have no side effects. Configurable allows parameter to permit LOGGING, METRICS, IO, or NONDETERMINISM. Also recognises @SideEffectFree, @Contract(pure = true), and @Memoized.

GROOVY-11914

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:

  1. deposit(100): @Requires met (100 > 0), @Ensures gives balance == old + 100, @Modifies proves only balance and log changed

  2. withdraw(30): @Requires met (30 > 0, 30 within balance), @Ensures gives balance == 100 - 30 = 70, @Modifies proves withdraw didn’t undo the deposit

  3. available(): @Pure proves no side effects — just returns balance (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 deposit change anything besides balance and log?

No — @Modifies proves it

Must read body + all callees

Does withdraw undo the deposit?

No — @Modifies + @Ensures prove independence

Must read both method bodies

Is available() side-effect-free?

Yes — @Pure proves it (verified by PurityChecker)

Must read body, check for overrides

What is balance after all three calls?

Derive from @Ensures chain: 0 + 100 - 30 = 70

Replay all mutations manually

Can deposit/withdraw be reordered safely?

Check @Modifies sets + @Requires preconditions

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

+=

plusAssign

plus

-=

minusAssign

minus

*=

multiplyAssign

multiply

/=

divAssign

div

%=

remainderAssign

remainder

**=

powerAssign

power

<⇐

leftShiftAssign

leftShift

>>=

rightShiftAssign

rightShift

>>>=

rightShiftUnsignedAssign

rightShiftUnsigned

&=

andAssign

and

|=

orAssign

or

^=

xorAssign

xor

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

isSorted()

Check whether elements of an Iterable, Iterator, array, or Map are in sorted order. Supports natural ordering, Comparator, or Closure.

GROOVY-11891

execute(dir:, env:, …​)

Named parameters for process configuration: dir, env, redirectErrorStream, inheritIO, file redirection.

GROOVY-11901

toProcessBuilder()

Convert a String, String array, or List into a ProcessBuilder for fluent process configuration.

GROOVY-11901

pipeline()

Create native OS pipelines from a list of commands via ProcessBuilder#startPipeline().

GROOVY-11901

onExit { }

Register a closure to execute asynchronously when a process terminates. Process.onExit() returns a CompletableFuture, so it composes naturally with await.

GROOVY-11901

textAsync / bytesAsync

Asynchronous file reading on Path, returning CompletableFuture.

GROOVY-11902

writeAsync() / writeBytesAsync()

Asynchronous file writing on Path, returning CompletableFuture.

GROOVY-11902

grepping(filter) / grepping()

Lazy Iterator variants of grep: filter elements using isCase (classes, patterns, ranges, closures, etc.) without materialising an intermediate collection. The no-arg form uses the IDENTITY closure for Groovy-truth filtering. Rounds out the -ing family alongside collecting, findingAll, and findingResults.

GROOVY-11596, GROOVY-11957

findGroups(regex) / findGroups(pattern)

Find the first occurrence of a regex within a CharSequence and return the full match followed by each capture group as a List (empty if no match). Multi-assignment pads missing elements with null, so the same destructuring works whether or not the regex matches.

GROOVY-11958

findAllGroups(regex) / findAllGroups(pattern)

Return a list of all matches of a regex within a CharSequence, with each match represented by a List containing the full match followed by each capture group.

GROOVY-11958

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

groupBy

Disables all groupBy overloads

groupBy(MutableList)

Disables only the overload for MutableList receivers

groupBy,countBy

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 + as coercion (no deps); ObjectMapper for advanced cases

JsonOutput.toJson()

CSV (GROOVY-11923)

CsvSlurper.parseAs(Type, csv) (Jackson)

CsvBuilder.toCsv(items, Type)

TOML (GROOVY-11925)

TomlSlurper.parseTextAs(Type, toml) (Jackson)

TomlBuilder.toToml(object)

YAML (GROOVY-11926)

YamlSlurper.parseTextAs(Type, yaml) (Jackson)

YamlBuilder.toYaml(object)

XML (GROOVY-11927)

XmlParser.parseTextAs(Type, xml) (optional Jackson); Node.toMap() + as coercion (no deps)

 — 

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

Apache Ivy

Maven Resolver (Aether)

Engine class

groovy.grape.ivy.GrapeIvy

groovy.grape.maven.GrapeMaven

Local cache

~/.groovy/grapes (Ivy format)

~/.groovy/grapesM2 (Maven format)

Configuration file

~/.groovy/grapeConfig.xml (full Ivy XML)

None — use @GrabResolver / Grape.addResolver

conf: parameter

Ivy configurations; lists supported (e.g. ['default','optional'])

Single Maven scope only

@GrabConfig(autoDownload=false)

Honoured (cache-only resolution)

Not honoured

Version wildcard '*'

Resolves to Ivy’s latest.default

Resolves to Maven’s LATEST

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 @GrabResolver at a local-only repository for similar semantics.

  • ~/.groovy/grapeConfig.xml settings are GrapeIvy-only — register custom repositories via @GrabResolver or Grape.addResolver instead.

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):

  1. A file specified via -Djava.util.logging.config.file=…​

  2. A user configuration at ~/.groovy/logging.properties (auto-discovered by Groovy at startup)

  3. 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 OS prefers-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).

Groovydoc light theme

Groovydoc dark theme

Groovydoc custom theme

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.

GDK light theme

GDK dark theme

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.

GroovyConsole light theme

GroovyConsole dark theme

GroovyConsole custom theme

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 OS prefers-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

overview-tree.html and per-package package-tree.html with class/interface nesting (GROOVY-11942)

Script documentation

Top-level scripts now get documented; leading /** */ comments lift to the synthetic Script class (GROOVY-8877)

@Internal filtering

Members annotated with groovy.transform.@Internal (per GEP-17) are hidden by default; -showInternal opts them in (GROOVY-9572)

{@value} inline tag

Inlines compile-time constants. Supports {@value #FIELD}, {@value Class#FIELD}, and bare {@value} on a field’s own comment (GROOVY-6016)

{@inheritDoc}

Pulls in the parent class’s/interface’s corresponding documentation (GROOVY-3782)

@apiNote, @implSpec, @implNote

Recognised with proper heading labels (e.g. "Implementation Requirements:" rather than raw tag names) (GROOVY-11945)

@Documented filter on annotation refs

Annotation references are only emitted when the annotation type itself is @Documented, matching Javadoc (GROOVY-4634)

doc-files/ and snippet-files/

Per-package directories are copied verbatim to the output for sibling images, diagrams, and external snippet sources (GROOVY-5986)

--add-stylesheet

Add a custom stylesheet alongside the default (contrast with -stylesheetfile which replaces); repeatable or comma-separated (GROOVY-11941)

Optional-page toggles

-noindex, -nodeprecatedlist, -nohelp suppress the matching auxiliary page and remove its nav-bar link (GROOVY-11943)

Ant <link module="…​">

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 Foo.1.html pages for enums with per-constant bodies (GROOVY-10162); NPE on closures in annotations fixed (GROOVY-8025); long-standing stray-brace bug in {@inheritDoc} rendering resolved (GROOVY-3782)

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:

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

  • ASTTransformationCustomizer now supports annotations whose @GroovyASTTransformationClass lists multiple transforms (e.g. @Sealed, @RecordBase) and @AnnotationCollector aliases (e.g. @AutoExternalize), via a new forAnnotation(…​) static factory that returns one customizer per resolved transform class (GROOVY-11973). For example: config.addCompilationCustomizers(*ASTTransformationCustomizer.forAnnotation(Sealed)).

Other Module Changes

  • groovy-xml: XmlParser and XmlSlurper now support named parameter construction for all parser options including validating, namespaceAware, and allowDocTypeDeclaration (GROOVY-7633). For example: new XmlParser(namespaceAware: false, trimWhitespace: true).

  • groovy-xml: XmlUtil.serialize now accepts a SerializeOptions parameter 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.FactorySupport is extended to cover all six JAXP factory types (adds XMLInputFactory, TransformerFactory, SchemaFactory and XPathFactory alongside the existing DocumentBuilderFactory and SAXParserFactory), with explicit allowDocTypeDeclaration / allowExternalResources overloads where appropriate. XmlUtil.events(reader) returns a Stream<XMLEvent> over a hardened XMLInputFactory; XmlUtil.streamElements(reader, [namespaceURI,] localName) pulls each matching subtree as a small DOM Node, suitable for streaming multi-gigabyte feeds without loading them into memory. SerializeOptions adds an allowExternalResources field; DOMBuilder.newInstance adds a 3-arg overload taking allowDocTypeDeclaration. A new XML security chapter in the XML user guide documents the secure-by-default contract end-to-end. (GROOVY-11979).

  • groovy-sql: The DataSet class now correctly handles non-literal expressions in queries (GROOVY-5373).

  • groovy-sql: New Sql.inList helper expands a collection into the parameter list of a SQL IN clause, avoiding manual string concatenation (GROOVY-5436).

  • groovy-sql: Added Map-based named-parameter overloads for call, callWithRows, and callWithAllRows, matching the existing named-parameter support on eachRow, 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

  • val is now a contextual keyword for declaring final variables (see val Keyword for Final Declarations). A few pre-existing edge cases around the sibling var keyword now apply to val as well — chiefly a field named val declared immediately before a method or constructor, and val as Type cast expressions. The -Dgroovy.val.enabled=false system 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 methodMissing and propertyMissing protocol 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 class invokeMethod/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 incorrect GroovyCastException errors at runtime but may change which setter is invoked if your code relied on the previous behavior. (GROOVY-11829)

  • TomlSlurper and YamlSlurper no longer prematurely close Reader and InputStream arguments passed to parsing methods. Callers are now responsible for closing resources they create, following standard conventions. (GROOVY-11925, GROOVY-11926)

  • XmlParser.parse(File) and XmlParser.parse(Path) now properly close the underlying InputStream after parsing. Previously, the stream was not closed, which could cause file descriptor leaks. (GROOVY-11927)

  • XmlParser.setNamespaceAware(boolean) now throws IllegalStateException if 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 static DOMBuilder.parse(…​) overloads, XmlUtil.newSAXParser) were already secure-by-default and are unaffected. (1) XmlUtil.serialize now blocks external <xsl:import>/<xsl:include> and external DTD references in the underlying TransformerFactory. Affects callers passing XSLT documents with external resource references through serialize; opt back in via SerializeOptions.allowExternalResources=true. The common case of pretty-printing already-parsed nodes or DOM trees is unaffected. (2) FactorySupport.createDocumentBuilderFactory() and createSaxParserFactory() 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() and newInstance(validating, namespaceAware) now return a builder backed by a hardened factory. The DSL-build path doesn’t parse external input and parseText already routed through the hardened static parse, so this only bites code that reaches into domBuilder.documentBuilder and parses DOCTYPE-bearing XML directly. New newInstance(validating, namespaceAware, allowDocTypeDeclaration) overload provides a relax knob. (GROOVY-11981)

  • The last references to javax.swing.JApplet have been removed from groovy-swing (the SwingBuilder factory registration) and from the Groovy Console. JApplet was deprecated for removal since Java 9 and has been removed in JDK 26, so these references would otherwise cause NoClassDefFoundError at SwingBuilder initialisation on JDK 26+. Code that explicitly used the applet-related factories should migrate to standard JFrame/JWindow alternatives. (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.