This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch asf-site
in repository https://gitbox.apache.org/repos/asf/groovy-website.git
The following commit(s) were added to refs/heads/asf-site by this push:
new 1feb208 release notes update: async/await, some tidying
1feb208 is described below
commit 1feb208bfdb5f952e887e713afde0380ffc219e0
Author: Paul King <[email protected]>
AuthorDate: Tue Apr 14 09:32:48 2026 +1000
release notes update: async/await, some tidying
---
site/src/site/releasenotes/groovy-6.0.adoc | 656 ++++++++++++++++-------------
1 file changed, 366 insertions(+), 290 deletions(-)
diff --git a/site/src/site/releasenotes/groovy-6.0.adoc
b/site/src/site/releasenotes/groovy-6.0.adoc
index f792b9b..acc0512 100644
--- a/site/src/site/releasenotes/groovy-6.0.adoc
+++ b/site/src/site/releasenotes/groovy-6.0.adoc
@@ -21,168 +21,285 @@ Some features described here as "incubating" may become
stable before 6.0.0 fina
TBD
-== Extension method additions and improvements
+== Native Async/Await (incubating)
-Groovy provides over 2000 extension methods to 150+ JDK classes to enhance JDK
functionality, with new methods added in Groovy 6. These methods reduce
dependency on third-party libraries for common tasks, and make code more
intuitive. Let's explore some highlights from those new methods.
+Groovy 6 adds native `async`/`await` support
+(https://issues.apache.org/jira/browse/GROOVY-9381[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.
-=== Collections and arrays
+See also the https://groovy.apache.org/blog/groovy-async-await[async/await
blog post]
+for a detailed walkthrough.
-Several variants of `groupByMany`
-(https://issues.apache.org/jira/browse/GROOVY-11808[GROOVY-11808])
-exist for grouping lists and maps of items.
+=== Before and after
-The most common form takes a closure (or lambda) which converts from some item
-into a list of keys to group by (in this case group words by the vowels they
contain):
+Without `async`/`await`, concurrent code requires chaining futures:
[source,groovy]
----
-var words = ['ant', 'bee', 'ape', 'cow', 'pig']
-
-var vowels = 'aeiou'.toSet()
-var vowelsOf = { String word -> word.toSet().intersect(vowels) }
+// Before: CompletableFuture chains
+def future = CompletableFuture.supplyAsync { loadUserProfile(id) }
+ .thenCompose { profile -> CompletableFuture.supplyAsync {
loadQuests(profile) } }
+ .thenApply { quests -> quests.find { it.active } }
-assert words.groupByMany(s -> vowelsOf(s)) == [
- a:['ant', 'ape'], e:['bee', 'ape'], i:['pig'], o:['cow']
-]
+def quest = future.join()
----
-The most general form takes two closures, one to transform the item to some
list of keys for grouping,
-the other to transform the grouped value if needed:
+With `async`/`await`, the same logic reads like synchronous code:
[source,groovy]
----
-record Person(String name, List<String> citiesLived) { }
-
-def people = [
- new Person('Alice', ['NY', 'LA']),
- new Person('Bob', ['NY']),
- new Person('Cara', ['LA', 'CHI'])
-]
-
-def grouped = people.groupByMany(Person::name, Person::citiesLived)
-
-assert grouped == [
- NY : ['Alice', 'Bob'],
- LA : ['Alice', 'Cara'],
- CHI : ['Cara']
-]
+// 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 }
+}
----
-Variants also exist for maps where the value is already a list.
+Exception handling works with standard `try`/`catch` -- no `.exceptionally()`
chains.
+
+=== Parallel tasks and combinators
-The `isSorted` method
-(https://issues.apache.org/jira/browse/GROOVY-11891[GROOVY-11891])
-checks whether the elements of an Iterable, Iterator, array, or Map
-are in sorted order. It works with natural ordering, a `Comparator`,
-or a `Closure`:
+Launch tasks concurrently and coordinate results:
[source,groovy]
----
-assert [1, 2, 3].isSorted()
-assert !([3, 1, 2].isSorted())
-assert ['hi', 'hey', 'hello'].isSorted { it.length() }
-----
+def a = async { fetchFromServiceA() }
+def b = async { fetchFromServiceB() }
+def c = async { fetchFromServiceC() }
-=== Process handling
+// Wait for all three
+def (resultA, resultB, resultC) = await(a, b, c)
+----
-Groovy's existing process methods predate Java's `ProcessBuilder`.
-Groovy 6 adds several new methods to alleviate some of the pain points
-of the existing functionality and make using `ProcessBuilder` more
Groovy-friendly
-(https://issues.apache.org/jira/browse/GROOVY-11901[GROOVY-11901]).
+=== Generators with `yield return`
-Previously, process execution often involved manually handling input/output
streams,
-waiting for the process to complete, and dealing with exit codes.
-The `waitForResult` method manages all the parts and returns a `ProcessResult`
object:
+An `async` closure containing `yield return` becomes a lazy generator --
+it produces values on demand with natural back-pressure:
[source,groovy]
----
-var result = 'echo Hello World'.execute().waitForResult()
-assert result.output == 'Hello World\n'
-assert result.exitValue == 0
+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]
----
-A timeout variant forcibly destroys the process
-if it doesn't complete within the specified duration:
+=== Channels
+
+Go-style inter-task communication. A producer sends values into a channel;
+a consumer receives them:
[source,groovy]
----
-var result = 'sleep 60'.execute().waitForResult(5, TimeUnit.SECONDS)
-----
+def ch = AsyncChannel.create(5) // buffered channel
-The `execute` method now also supports named parameters for process
-configuration, including options like `dir`, `env`,
-`redirectErrorStream`, `inheritIO`, and file redirection:
+async {
+ for (i in 1..10) ch.send(i)
+ ch.close()
+}
-[source,groovy]
-----
-var result = 'ls'.execute(dir: '/tmp', redirectErrorStream: true)
+for (val in ch) { println val } // prints 1..10
----
-The `toProcessBuilder` method converts a String, String array,
-or List into a `ProcessBuilder`, giving access to its full
-fluent API for more advanced configuration:
+=== Structured concurrency
+
+`AsyncScope` binds the lifetime of child tasks to a scope -- when the scope
+exits, all children are guaranteed complete or cancelled:
[source,groovy]
----
-var ls = 'ls'.toProcessBuilder()
- .directory(new File('/tmp'))
- .redirectErrorStream(true)
- .start()
+AsyncScope.run {
+ def users = async { loadUsers() }
+ def config = async { loadConfig() }
+ processResults(await(users), await(config))
+}
+// Both tasks guaranteed complete here
----
-Earlier Groovy versions support the `|` operator to simulate OS pipelines via
thread-based stream copying, and providing a handle to the last stage of the
pipeline.
-The `pipeline` method improves this by leveraging
`ProcessBuilder#startPipeline()`
-to create native OS pipelines from a list of
-commands, and supports proper handles for all pipeline stages, not just the
last one:
+=== Feature summary
+
+[cols="1,3", options="header"]
+|===
+| 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
+|===
+
+== 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`
+(https://issues.apache.org/jira/browse/GROOVY-11808[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:
[source,groovy]
----
-var processes = ['ps aux', 'grep java', 'wc -l'].pipeline()
+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']
+]
----
-As a small convenience, the `onExit` method registers a closure to execute
asynchronously
-when a process terminates:
+For maps whose values are already lists, a no-args variant groups keys by
their values:
[source,groovy]
----
-'some command'.execute().onExit { proc ->
- println "Exited with: ${proc.exitValue()}"
-}
+var availability = [
+ '🍎': ['Spring'],
+ '🍌': ['Spring', 'Summer', 'Autumn', 'Winter'],
+ '🍇': ['Spring', 'Autumn'],
+ '🍒': ['Autumn'],
+ '🍑': ['Spring']
+]
+
+assert availability.groupByMany() == [
+ Winter: ['🍌'],
+ Autumn: ['🍌', '🍇', '🍒'],
+ Summer: ['🍌'],
+ Spring: ['🍎', '🍌', '🍇', '🍑']
+]
----
-=== Asynchronous file I/O
+A two-closure form also exists for transforming both keys and values.
+See the https://groovy.apache.org/blog/fruity-eclipse-grouping[groupByMany
blog post]
+for more examples including Eclipse Collections interop.
-The `groovy-nio` module adds asynchronous file I/O extension methods
-on `java.nio.file.Path`
-(https://issues.apache.org/jira/browse/GROOVY-11902[GROOVY-11902]).
-These methods leverage `AsynchronousFileChannel` for non-blocking
-file operations and return `CompletableFuture` results.
+=== Process handling
-Reading methods:
+`waitForResult` replaces the manual stream/exit-code dance with a single call
+(https://issues.apache.org/jira/browse/GROOVY-11901[GROOVY-11901]):
[source,groovy]
----
-import java.nio.file.Path
+var result = 'echo Hello World'.execute().waitForResult()
+assert result.output == 'Hello World\n'
+assert result.exitValue == 0
-var path = Path.of('data.txt')
-var textFuture = path.textAsync // CompletableFuture<String>
-var bytesFuture = path.bytesAsync // CompletableFuture<byte[]>
+// With timeout
+var result = 'sleep 60'.execute().waitForResult(5, TimeUnit.SECONDS)
----
-Writing methods:
+=== Asynchronous file I/O
+
+The `groovy-nio` module adds async file operations on `Path` that return
+`CompletableFuture` results
+(https://issues.apache.org/jira/browse/GROOVY-11902[GROOVY-11902]).
+These compose naturally with Groovy 6's `async`/`await`:
[source,groovy]
----
-path.writeAsync('Hello async!') // CompletableFuture<Void>
-path.writeBytesAsync(bytes) // CompletableFuture<Void>
+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)
----
-These futures support natural composition using `CompletableFuture` methods:
+=== Other new extension methods
-[source,groovy]
-----
-path.textAsync.thenApply { it.toUpperCase() }.thenAccept { println it }
-----
+[cols="2,3,1", options="header"]
+|===
+| Method | Description | Ticket
+
+| `isSorted()`
+| Check whether elements of an Iterable, Iterator, array, or Map
+are in sorted order. Supports natural ordering, `Comparator`, or `Closure`.
+| https://issues.apache.org/jira/browse/GROOVY-11891[GROOVY-11891]
+
+| `execute(dir:, env:, ...)`
+| Named parameters for process configuration: `dir`, `env`,
+`redirectErrorStream`, `inheritIO`, file redirection.
+| https://issues.apache.org/jira/browse/GROOVY-11901[GROOVY-11901]
+
+| `toProcessBuilder()`
+| Convert a String, String array, or List into a `ProcessBuilder`
+for fluent process configuration.
+| https://issues.apache.org/jira/browse/GROOVY-11901[GROOVY-11901]
+
+| `pipeline()`
+| Create native OS pipelines from a list of commands via
+`ProcessBuilder#startPipeline()`.
+| https://issues.apache.org/jira/browse/GROOVY-11901[GROOVY-11901]
+
+| `onExit { }`
+| Register a closure to execute asynchronously when a process terminates.
+| https://issues.apache.org/jira/browse/GROOVY-11901[GROOVY-11901]
+
+| `textAsync` / `bytesAsync`
+| Asynchronous file reading on `Path`, returning `CompletableFuture`.
+| https://issues.apache.org/jira/browse/GROOVY-11902[GROOVY-11902]
+
+| `writeAsync()` / `writeBytesAsync()`
+| Asynchronous file writing on `Path`, returning `CompletableFuture`.
+| https://issues.apache.org/jira/browse/GROOVY-11902[GROOVY-11902]
+|===
== Selectively Disabling Extension Methods
@@ -323,17 +440,19 @@ to revert back to GrapeIvy.
*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.
-== HttpBuilder: Lightweight HTTP Client DSL (incubating)
+== HttpBuilder: HTTP Client Module (incubating)
Groovy 6 introduces a new `groovy-http-builder` module
-(https://issues.apache.org/jira/browse/GROOVY-11879[GROOVY-11879])
-providing a declarative DSL over the JDK's `java.net.http.HttpClient`.
-It is designed for scripting and simple automation,
+(https://issues.apache.org/jira/browse/GROOVY-11879[GROOVY-11879],
+https://issues.apache.org/jira/browse/GROOVY-11924[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.
-=== Quick start
+=== Imperative DSL
-Create a client with a base URI and start making requests:
+A closure-based DSL for quick scripting:
[source,groovy]
----
@@ -344,125 +463,134 @@ def result = client.get('/repos/apache/groovy')
assert result.json.license.name == 'Apache License 2.0'
----
-=== Configuring the client
+Responses auto-parse by content type: `result.json`, `result.xml`,
+`result.html` (via jsoup), or `result.parsed` for auto-dispatch.
-Use a configuration closure to set default headers, timeouts, and redirect
behavior:
+=== 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:
[source,groovy]
----
-import groovy.http.HttpBuilder
-import java.time.Duration
+@HttpBuilderClient('https://api.example.com')
+interface UserApi {
+ @Get('/users/{id}')
+ User getUser(String id) // path param: {id}
-def client = HttpBuilder.http {
- baseUri 'https://api.example.com'
- header 'User-Agent', 'my-app/1.0'
- connectTimeout Duration.ofSeconds(5)
- followRedirects true
-}
-----
+ @Get('/users')
+ List<User> search(String name) // implied query param: ?name=...
-=== JSON POST
+ @Post('/users')
+ User create(@Body Map user) // JSON body
-[source,groovy]
-----
-def result = client.post('/api/items') {
- json([name: 'book', qty: 2])
+ @Post('/login')
+ @Form
+ Map login(String username, String password) // form-encoded
}
-assert result.status == 200
-assert result.json.ok
+
+def api = UserApi.create()
+def user = api.getUser('42')
----
-=== Form POST
+=== Async support
+
+Both sides offer native async via `HttpClient.sendAsync()` -- no
+extra threads consumed while waiting:
[source,groovy]
----
-def result = client.post('/login') {
- form(username: 'admin', password: 's3cret')
+// 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)
}
-assert result.status == 200
----
-=== Query parameters
+If the `groovy-async` module is on the classpath, these are
+automatically `await`-able: `def data = await api.getDataAsync('42')`.
-[source,groovy]
-----
-def result = client.get('/api/items') {
- query page: 1, size: 10
-}
-----
+=== Feature summary
-=== XML responses
+[cols="2,1,1", options="header"]
+|===
+| Feature | Imperative | Declarative
-[source,groovy]
-----
-def result = client.get('/api/repo.xml')
-assert result.xml.license.text() == 'Apache License 2.0'
-----
+| HTTP methods (GET, POST, PUT, DELETE, PATCH)
+| All
+| All (`@Get`, `@Post`, `@Put`, `@Delete`, `@Patch`)
-=== HTML scraping with jsoup
+| JSON body / response
+| `json()` / `result.json`
+| `@Body` / return-type driven
-When https://jsoup.org/[jsoup] is on the classpath,
-HTML responses are automatically parsed into a jsoup `Document`:
+| Form-encoded body
+| `form()`
+| `@Form`
-[source,groovy]
-----
-@Grab('org.jsoup:jsoup:1.22.1')
-import static groovy.http.HttpBuilder.http
+| Plain text body
+| `text()`
+| `@BodyText`
-def client = http('https://example.com')
-def result = client.get('/page')
-def heading = result.html.select('h1').text()
-----
+| XML / HTML response
+| `result.xml` / `result.html`
+| `GPathResult` / jsoup `Document` return type
-=== Response parsing
+| Typed response objects
+| Manual (`result.json as User`)
+| Automatic (return type driven)
-`HttpResult` provides convenient accessors for common content types:
+| Query parameters
+| `query()`
+| Implied from parameter name (or `@Query`)
-- `result.json` -- parsed via `JsonSlurper`
-- `result.xml` -- parsed via `XmlSlurper`
-- `result.html` -- parsed via jsoup (if available)
-- `result.parsed` -- auto-dispatches based on the response `Content-Type`
-- `result.body` -- the raw response body as a `String`
+| Path parameters
+| Manual
+| Auto-mapped via `{name}` placeholders
-=== Declarative HTTP client
+| Headers
+| `header()` in config/request
+| `@Header` on interface/method
-An annotation-driven declarative HTTP client
-(https://issues.apache.org/jira/browse/GROOVY-11924[GROOVY-11924])
-is also available, inspired by similar features in Micronaut and Retrofit.
-Annotate an interface with `@HttpBuilderClient` and method-level
-annotations like `@Get`, `@Post`, `@Put`, `@Delete`, and `@Patch`:
+| Async
+| `getAsync()`, `postAsync()`, etc.
+| `CompletableFuture<T>` return type
-[source,groovy]
-----
-@HttpBuilderClient('https://api.github.com')
-interface GitHubApi {
- @Get('/repos/{owner}/{repo}')
- def getRepo(String owner, String repo)
+| Timeouts (connect / request)
+| Config DSL
+| `connectTimeout`, `requestTimeout` on `@HttpBuilderClient`
- @Post('/repos/{owner}/{repo}/issues')
- @Header(name = 'Accept', value = 'application/json')
- def createIssue(String owner, String repo, @Body Map issue)
-}
-----
+| 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
(https://issues.apache.org/jira/browse/GROOVY-11878[GROOVY-11878]).
-Since the JVM does not support annotations on statements in bytecode,
-these are purely source-level transforms: the annotation drives
-compile-time code generation and is then discarded.
-
-Several transforms take advantage of this capability,
-including `@Invariant` and `@Decreases` from `groovy-contracts`
-(see the <<groovy-contracts>> section for details)
-and the `@Parallel` transform described below.
-
-=== `@Parallel` on for-in loops
-
-The `@Parallel` transform runs each iteration of a for-in loop
-in its own thread:
+Several transforms take advantage of this, including `@Invariant` and
+`@Decreases` from `groovy-contracts` (see <<groovy-contracts>>)
+and `@Parallel`:
[source,groovy]
----
@@ -473,18 +601,9 @@ for (int i in 1..4) {
// Output (non-deterministic order): 1, 16, 9, 4
----
-When running on JDK 21+, `@Parallel` will use virtual threads
-for lightweight concurrency; on earlier JDKs it falls back to platform threads.
-
-NOTE: `@Parallel` is an incubating transform favoring simplicity.
-Production use should consider proper concurrency mechanisms.
-
-=== Writing custom statement-level transforms
-
-Any source-retention AST transform can now target `STATEMENT_TARGET`.
-The transform's `visit` method receives the `AnnotationNode` and the
-`Statement` (a `LoopingStatement`) as its AST node pair, following the
-same contract as existing class/method/field-level transforms.
+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]]
== Groovy-Contracts Enhancements (incubating)
@@ -1099,9 +1218,7 @@ previously relied on the lenient behavior.
GINQ's `groupby` clause now supports an `into` keyword
(https://issues.apache.org/jira/browse/GROOVY-11915[GROOVY-11915])
-that binds each group to a named variable,
-giving direct access to the grouped elements
-within the `select` or `having` clause:
+that binds each group to a named variable with aggregate access:
[source,groovy]
----
@@ -1121,11 +1238,11 @@ GQ {
+------+-------+-----+--------------+
----
-=== Set operators: `union`, `intersect`, `minus`, `unionall`
+=== Set operators
-GINQ now supports SQL-style set operators
+SQL-style set operators
(https://issues.apache.org/jira/browse/GROOVY-11919[GROOVY-11919])
-for combining query results:
+for combining query results: `union`, `intersect`, `minus`, and `unionall`:
[source,groovy]
----
@@ -1136,23 +1253,9 @@ assert GQL {
from n in java select n
union
from n in groovy select n
-} == ['Alice', 'Bob', 'Carol', 'Dave'] // everyone
-
-assert GQL {
- from n in java select n
- intersect
- from n in groovy select n
-} == ['Bob', 'Carol'] // know both
-
-assert GQL {
- from n in java select n
- minus
- from n in groovy select n
-} == ['Alice'] // Java only
+} == ['Alice', 'Bob', 'Carol', 'Dave']
----
-The `unionall` variant is like `union` but retains duplicates.
-
== CSV Module (incubating)
Groovy 6 adds a new `groovy-csv` module
@@ -1210,91 +1313,64 @@ assert sales[0].amount == 1500.00
== Typed Parsing and Writing Across Format Modules
-Groovy 6 brings typed parsing and writing support across all
-data format modules -- JSON, XML, CSV, TOML, and YAML -- giving
-a consistent way to convert between structured data and typed objects.
-
-=== JSON typed coercion
-
-Since `JsonSlurper` parses JSON values into their natural JVM types
-(`Integer`, `BigDecimal`, `Boolean`, `List`, `Map`), Groovy's built-in
-`as` coercion works directly for converting parsed JSON into typed objects,
-including nested objects and enums:
+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:
[source,groovy]
----
-class ServerConfig {
- String host
- int port
- boolean debug
-}
-
-def config = new JsonSlurper().parseText(
- '{"host":"localhost","port":8080,"debug":true}'
-) as ServerConfig
-assert config.host == 'localhost'
-assert config.port == 8080
+class ServerConfig { String host; int port; boolean debug }
----
-For advanced cases (typed collections, date parsing, `@JsonProperty`),
-use `jackson-databind` directly via `new ObjectMapper().readValue(json, Type)`.
-
-=== CSV, TOML, and YAML typed parsing
-
-`CsvSlurper`, `TomlSlurper`, and `YamlSlurper` each provide `parseTextAs`
-and `parseAs` methods
-(https://issues.apache.org/jira/browse/GROOVY-11923[GROOVY-11923],
-https://issues.apache.org/jira/browse/GROOVY-11925[GROOVY-11925],
-https://issues.apache.org/jira/browse/GROOVY-11926[GROOVY-11926])
-that parse content directly into typed objects using Jackson databinding.
-Standard Jackson annotations such as `@JsonProperty` and `@JsonFormat`
-are supported for property mapping and type conversion:
+Each format can parse directly into it:
[source,groovy]
----
-class Config {
- String name
- int port
-}
+// JSON — as coercion (no extra deps)
+def config = new JsonSlurper().parseText(json) as ServerConfig
-def config = new TomlSlurper().parseTextAs(Config, 'name = "app"\nport = 8080')
-assert config.name == 'app'
-assert config.port == 8080
+// TOML — Jackson-backed parseTextAs
+def config = new TomlSlurper().parseTextAs(ServerConfig, toml)
+
+// XML — Jackson-backed parseTextAs
+def config = new XmlParser().parseTextAs(ServerConfig, xml)
----
-NOTE: For simple cases, Groovy's `as` coercion already works with the
-untyped result, e.g. `new TomlSlurper().parseText(toml) as Config`.
-The `parseTextAs`/`parseAs` methods use Jackson databinding for richer
-annotation-driven mapping.
+[cols="1,2,2", options="header"]
+|===
+| Format | Typed Parsing | Typed Writing
-`CsvBuilder.toCsv(items, type)`, `TomlBuilder.toToml(object)`, and
-`YamlBuilder.toYaml(object)` serialize typed objects directly.
+| JSON
+| `as` coercion (no deps); `ObjectMapper` for advanced cases
+| `JsonOutput.toJson()`
-=== XML typed parsing
+| CSV
+(https://issues.apache.org/jira/browse/GROOVY-11923[GROOVY-11923])
+| `CsvSlurper.parseAs(Type, csv)` (Jackson)
+| `CsvBuilder.toCsv(items, Type)`
-`XmlParser` gains `toMap()`, `parseTextAs`, and `parseAs` methods
-(https://issues.apache.org/jira/browse/GROOVY-11927[GROOVY-11927]).
-`Node.toMap()` converts an XML node tree into a nested `Map<String, Object>`
-with zero dependencies, and works with Groovy's `as` coercion for
-String-typed target properties. The `parseTextAs` and `parseAs` methods
-provide full type conversion via Jackson's `ObjectMapper.convertValue`
-(requires `jackson-databind` on the classpath at runtime):
+| TOML
+(https://issues.apache.org/jira/browse/GROOVY-11925[GROOVY-11925])
+| `TomlSlurper.parseTextAs(Type, toml)` (Jackson)
+| `TomlBuilder.toToml(object)`
-[source,groovy]
-----
-def config = new XmlParser().parseTextAs(ServerConfig, '''
- <server>
- <host>localhost</host>
- <port>8080</port>
- <debug>true</debug>
- </server>''')
-assert config.host == 'localhost'
-assert config.port == 8080
-----
+| YAML
+(https://issues.apache.org/jira/browse/GROOVY-11926[GROOVY-11926])
+| `YamlSlurper.parseTextAs(Type, yaml)` (Jackson)
+| `YamlBuilder.toYaml(object)`
+
+| XML
+(https://issues.apache.org/jira/browse/GROOVY-11927[GROOVY-11927])
+| `XmlParser.parseTextAs(Type, xml)` (optional Jackson);
+`Node.toMap()` + `as` coercion (no deps)
+| --
+|===
-For the full Jackson XML experience (e.g. `@JacksonXmlText`,
-`@JacksonXmlElementWrapper`), use `jackson-dataformat-xml` directly:
-`new XmlMapper().readValue(xmlString, ServerConfig)`.
+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.
== Other Module Changes