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

TBD

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

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.

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

If the groovy-async module is on the classpath, these are automatically await-able: 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). Several transforms take advantage of this, including @Invariant and @Decreases from groovy-contracts (see Groovy-Contracts Enhancements (incubating)) and @Parallel:

@Parallel
for (int i in 1..4) {
    println i ** 2
}
// Output (non-deterministic order): 1, 16, 9, 4

On JDK 21+, @Parallel uses virtual threads; on earlier JDKs it falls back to platform threads. Custom statement-level transforms can target STATEMENT_TARGET following the same contract as class/method/field-level transforms.

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.

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.

GROOVY-11890

@Modifies

Frame condition declaring which fields a method may change. Everything not listed is guaranteed unchanged. @Pure is shorthand for @Modifies({ [] }).

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.

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

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.

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

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

CSV Module (incubating)

Groovy 6 adds a new groovy-csv module (GROOVY-11923) for reading and writing CSV (RFC 4180) data.

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, and quoted fields follow RFC 4180 rules (embedded commas, newlines, and doubled quotes).

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

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

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.

Grape: Dual Engine Support (Incubating)

Groovy 6 introduces a major evolution of the Grape dependency management system (GROOVY-11871) with the addition of a second built-in engine implementation alongside the existing Apache Ivy-based engine.

What’s new

Two production-ready Grape engines

  • GrapeIvy (default): Apache Ivy backend, established and stable. Supports fine-grained configuration via ~/.groovy/grapeConfig.xml, multiple Ivy configurations per dependency, and optional-only downloads via @GrabConfig(autoDownload=false).

  • GrapeMaven: New Maven Resolver (Aether) backend. Uses the same resolution strategy as Maven itself, integrates with Maven Central and other standard Maven repositories out of the box, and provides an alternative for teams more familiar with Maven tooling.

Automatic engine selection with override capability

Both engines expose the same @Grab, @GrabResolver, and @GrabExclude annotations and APIs. If both are on the classpath, GrapeIvy is preferred for backward compatibility. Users can switch to GrapeMaven on a per-script basis:

groovy -Dgroovy.grape.impl=groovy.grape.maven.GrapeMaven yourscript.groovy

Extensibility via Java SPI

Developers can now provide custom Grape implementations by creating a jar with a META-INF/services/groovy.grape.GrapeEngine entry. Groovy will discover and load custom implementations via the standard Java Service Provider Interface, enabling integration with in-house or specialized dependency resolution engines.

Cache directories

  • GrapeIvy: ~/.groovy/grapes (Ivy cache format)

  • GrapeMaven: ~/.groovy/grapesM2 (Maven repository format)

Both respect the grape.root system property for custom cache root paths.

Compatibility notes

Most existing @Grab scripts remain unchanged and work with both engines, as they use only the common core features. However, there are some compatibility considerations when switching between engines:

GrapeIvy to GrapeMaven

  • conf: parameter: GrapeIvy maps conf: to Ivy configurations (which can be a list like conf:['default','optional']). GrapeMaven treats conf: as a Maven scope string (singular). Scripts using multiple configurations will need reworking or should remain on GrapeIvy.

  • Custom Ivy configuration: If you have customized ~/.groovy/grapeConfig.xml with non-standard settings (e.g., disabled m2compatible mode, custom resolvers), those settings do not apply to GrapeMaven. Review your configuration and ensure your scripts remain on GrapeIvy or move them to use @GrabResolver for repository configuration.

  • Optional-only downloads: @GrabConfig(autoDownload=false) is not honoured by GrapeMaven as a per-grab switch. The closest equivalent is to point @GrabResolver (or Grape.addResolver) at a local-only repository/cache location. If you rely on autoDownload=false semantics broadly, remain on GrapeIvy for now.

GrapeMaven to GrapeIvy

  • In most cases, switching from GrapeMaven to GrapeIvy is straightforward. GrapeIvy’s defaults are more permissive, so scripts that work with GrapeMaven typically work unchanged with GrapeIvy. GrapeMaven is new, so we don’t expect many users to need to switch back, but we mention it here for users who start migrating and might want to revert back to GrapeIvy.

Migration guide

For most users: No action required. Existing scripts continue to work with the default GrapeIvy engine.

If switching to GrapeMaven: Review the compatibility notes above. If your scripts use multiple conf: values, remain on GrapeIvy for now (the default) and start reworking your Grab definitions to singular scopes. For autoDownload=false workflows, point @GrabResolver to a local-only repository/cache location.

If you have customized Ivy settings: Your ~/.groovy/grapeConfig.xml is only honoured by GrapeIvy. If switching to GrapeMaven, you will need to reconfigure any custom repositories or settings using @GrabResolver annotations or programmatically via the Grape.addResolver() API.

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.

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, @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 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-sql: The DataSet class now correctly handles non-literal expressions in queries (GROOVY-5373).

  • 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

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

Under exploration

  • Java compatibility: Module import declarations, additional destructuring

  • Improve REPL further (think nushell)

  • Performance

  • Spec improvements

  • Further subprojects, e.g. maybe GPars

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.