Builders

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Groovy 6.0 (6.0.0, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26 (checked on 2026-10-14). A builder is an object that turns nested method calls with closures into a tree: XML, JSON, a graph of beans, a directory structure, a Swing user interface, an Ant build. Groovy has the pattern built in, in two ways: a family of ready-made builders, and two base classes, BuilderSupport and FactoryBuilderSupport, that make writing a new one short. Underneath, every builder is the closure delegation described on Closure Delegation: Owner, Delegate & This applied recursively, and builders are the basis for the DSLs on Domain Specific Languages. Kotlin reaches the same result with lambdas with receivers, see Kotlin Type-safe Builders & DSLs.

The idea: method calls become nodes

In a builder call such as zone(id: 'bench') { plant('Tomato') } there is no method called zone. The builder intercepts the call (through invokeMethod or methodMissing), treats the method name as the node name, the map as its attributes, a plain argument as its value and the closure as its children. Before it runs the closure it makes itself (or the node) the delegate, so the inner call plant(...) is resolved on the builder again, one level deeper. This is a complete builder in about twenty lines:

// A builder from scratch: method names become nodes, closures become children.
class TagBuilder {
    private StringBuilder out = new StringBuilder()
    private int depth = 0

    def methodMissing(String name, args) {
        def attrs = args.find { it instanceof Map } ?: [:]
        def body = args.find { it instanceof Closure }
        def text = args.find { it instanceof CharSequence }
        def attrText = attrs.collect { k, v -> " ${k}=\"${v}\"" }.join()
        out << '  ' * depth << "<${name}${attrText}>"
        if (body) {
            out << '\n'
            depth++
            body.delegate = this                       // nested names resolve on the builder
            body.resolveStrategy = Closure.DELEGATE_FIRST
            body()
            depth--
            out << '  ' * depth
        } else if (text) {
            out << text
        }
        out << "</${name}>\n"
        this
    }

    String toString() { out }
}

def html = new TagBuilder()
html.greenhouse(name: 'Home') {
    zone(id: 'bench') {
        plant('Tomato')
        plant('Basil')
    }
}
print html
<greenhouse name="Home">
  <zone id="bench">
    <plant>Tomato</plant>
    <plant>Basil</plant>
  </zone>
</greenhouse>

Real builders add structure: they separate creating a node from attaching it to its parent, and they handle the closure’s resolve strategy and the return value. The classes in this page do that for you.

The built-in builders at a glance

classDiagram GroovyObjectSupport <|-- BuilderSupport GroovyObjectSupport <|-- JsonBuilder GroovyObjectSupport <|-- StreamingJsonBuilder BuilderSupport <|-- NodeBuilder BuilderSupport <|-- MarkupBuilder BuilderSupport <|-- SAXBuilder BuilderSupport <|-- StaxBuilder BuilderSupport <|-- DOMBuilder BuilderSupport <|-- AntBuilder Binding <|-- FactoryBuilderSupport FactoryBuilderSupport <|-- SwingBuilder FactoryBuilderSupport <|-- ObjectGraphBuilder FactoryBuilderSupport <|-- JmxBuilder

The diagram shows the superclasses reported by Groovy 6.0.0 itself. StreamingMarkupBuilder (a separate abstract streaming base), FileTreeBuilder and CliBuilder (plain classes) are builders by usage rather than by inheritance.

Builder Produces Module and package

MarkupBuilder, StreamingMarkupBuilder

XML or HTML text

groovy-xml, groovy.xml

SAXBuilder, StaxBuilder, DOMBuilder

SAX events, a StAX stream, a W3C DOM tree

groovy-xml, groovy.xml

NodeBuilder

a tree of groovy.util.Node objects

core, groovy.util

JsonBuilder, StreamingJsonBuilder

JSON text

groovy-json, groovy.json

FileTreeBuilder

files and directories

core, groovy.util

ObjectGraphBuilder

a graph of JavaBean-style objects

core, groovy.util

CliBuilder

a command-line option parser

groovy-cli-commons or groovy-cli-picocli, groovy.cli.commons or groovy.cli.picocli

AntBuilder

runs Apache Ant tasks

groovy-ant, groovy.ant

SwingBuilder

Swing components

groovy-swing, groovy.swing

JmxBuilder

MBeans, connectors, timers and listeners

groovy-jmx, groovy.jmx.builder

All of these modules are in the lib directory of the binary distribution, so the examples below need no extra jars, with one exception noted for the picocli variant of CliBuilder.

XML builders

MarkupBuilder writes indented XML (or HTML) to a Writer; StreamingMarkupBuilder builds a template lazily with bind and writes when the result is converted to a string or written, and offers the mkp namespace for comments, raw text and declarations. Their options, namespaces and escaping are covered on XML: Creating & Manipulating; this is the shape of both.

import groovy.xml.MarkupBuilder
import groovy.xml.StreamingMarkupBuilder

def writer = new StringWriter()
def xml = new MarkupBuilder(writer)
xml.zones {
    zone(id: 'bench', area: 2.5) {
        plant(name: 'Tomato', 'needs a stake')
        plant(name: 'Basil')
    }
    zone(id: 'bed')
}
println writer

def streamed = new StreamingMarkupBuilder().bind {
    zones {
        zone(id: 'bench') { plant('Tomato') }
        mkp.comment('written lazily')
    }
}
println streamed.toString()
<zones>
  <zone id='bench' area='2.5'>
    <plant name='Tomato'>needs a stake</plant>
    <plant name='Basil' />
  </zone>
  <zone id='bed' />
</zones>
<zones><zone id='bench'><plant>Tomato</plant></zone><!--written lazily--></zones>

SAXBuilder, StaxBuilder and DOMBuilder use the same node notation to feed other XML APIs: SAXBuilder fires events at a ContentHandler, StaxBuilder writes to an XMLStreamWriter (which can be any StAX implementation, even one producing something other than XML), and DOMBuilder creates a W3C DOM tree that DOMCategory can then navigate. The class names are SAXBuilder and StaxBuilder in package groovy.xml.

import groovy.xml.DOMBuilder
import groovy.xml.SAXBuilder
import groovy.xml.StaxBuilder
import groovy.xml.dom.DOMCategory
import javax.xml.stream.XMLOutputFactory

// SAX events for a handler
class LogHandler extends org.xml.sax.helpers.DefaultHandler {
    List events = []
    void startElement(String uri, String localName, String qName, org.xml.sax.Attributes atts) { events << "start:${localName}" }
    void endElement(String uri, String localName, String qName) { events << "end:${localName}" }
}
def handler = new LogHandler()
new SAXBuilder(handler).zone { plant() }
println handler.events

// StAX writer
def writer = new StringWriter()
def stax = new StaxBuilder(XMLOutputFactory.newInstance().createXMLStreamWriter(writer))
stax.zone(id: 'bench') {
    plant('Tomato')
}
println writer

// W3C DOM tree built and read back
def doc = DOMBuilder.newInstance().zones {
    zone(id: 'bench') { plant('Tomato'); plant('Basil') }
}
println doc.tagName
use(DOMCategory) {
    println doc.zone.plant.collect { it.text() }
}
[start:zone, start:plant, end:plant, end:zone]
<?xml version='1.0' encoding='UTF-8'?><zone id="bench"><plant>Tomato</plant></zone>
zones
[Tomato, Basil]

NodeBuilder

NodeBuilder is the plainest builder: it creates groovy.util.Node objects that hold a name, a map of attributes and a list of children or a value, which is the same structure XmlParser returns. The tree is plain data that GPath expressions navigate.

def nodes = new NodeBuilder().zones {
    zone(id: 'bench', area: 2.5) {
        plant(name: 'Tomato')
        plant(name: 'Basil')
    }
    zone(id: 'bed') {
        plant(name: 'Carrot')
    }
}

println nodes.getClass().name
println nodes.zone.plant.'@name'.join(', ')
println nodes.zone.find { it.'@id' == 'bed' }.plant.size()
println nodes.zone[0].attributes()
println nodes.zone[0].plant.'@name'
groovy.util.Node
Tomato, Basil, Carrot
1
[id:bench, area:2.5]
[Tomato, Basil]

JSON builders

JsonBuilder creates the structure in memory, which suits output that is changed before it is written (as the assignment to json.content shows), and StreamingJsonBuilder writes straight to a Writer without the intermediate structure. A JsonGenerator passed to the constructor customises the output. Parsing, generators and JsonOutput are on JSON.

import groovy.json.JsonBuilder
import groovy.json.JsonOutput
import groovy.json.StreamingJsonBuilder

def json = new JsonBuilder()
json.zone {
    id 'bench'
    plants(['Tomato', 'Basil'])
    sensors {
        temperature 21.5
    }
}
println json.toString()
json.content.zone.id = 'bench-2'                      // an in-memory structure can still be changed
println JsonOutput.prettyPrint(json.toString())

def out = new StringWriter()
def streaming = new StreamingJsonBuilder(out)
streaming.zone(id: 'bed') {
    plants(['Carrot'])
}
println out
{"zone":{"id":"bench","plants":["Tomato","Basil"],"sensors":{"temperature":21.5}}}
{
    "zone": {
        "id": "bench-2",
        "plants": [
            "Tomato",
            "Basil"
        ],
        "sensors": {
            "temperature": 21.5
        }
    }
}
{"zone":{"id":"bed","plants":["Carrot"]}}

FileTreeBuilder

FileTreeBuilder creates directories and files under a base directory: dir(name) { ... } and file(name, text) are explicit, and in the shorthand form a method name becomes a directory and a name written as a string literal becomes a file. File reading and writing is on Files I/O & Paths.

def root = File.createTempDir('greenhouse', '')
try {
    def tree = new FileTreeBuilder(root)
    tree.dir('data') {
        file('plants.json', '[{"name":"Tomato"}]')
        dir('archive') {
            file('2026.csv', 'sensor,temperature\nT1,21.5\n')
        }
    }
    tree.config {                                  // shorthand: names become directories
        'zones.yaml'('bench: {}')
    }
    tree.file('README.txt', 'generated')           // top level

    def files = []
    root.eachFileRecurse { f -> if (f.isFile()) files << root.toPath().relativize(f.toPath()).toString() }
    println files.sort()
    assert new File(root, 'data/archive/2026.csv').readLines()[1] == 'T1,21.5'
} finally {
    root.deleteDir()
}
[README.txt, config/zones.yaml, data/archive/2026.csv, data/plants.json]

ObjectGraphBuilder

ObjectGraphBuilder builds a graph of JavaBean-style objects, which makes it handy for test data. It resolves a node name to a class (classNameResolver names the package), creates the instance with its no-argument constructor, sets the attributes as properties, and attaches each child to its parent: it adds to a collection property (plants) and fills the child’s property that points back to the parent (zone). The four strategies behind this (class name, new instance, relation name, child property setter) are replaceable, and id/refId attributes let several nodes share one object.

package greenhouse

class Zone {
    String name
    List plants = []
    Bed bed
}
class Plant {
    String name
    Zone zone
}
class Bed {
    String soil
}

def builder = new ObjectGraphBuilder()
builder.classLoader = this.class.classLoader
builder.classNameResolver = 'greenhouse'

def zone = builder.zone(name: 'bench') {
    bed(id: 'peat', soil: 'peat')
    plant(name: 'Tomato')
    plant(name: 'Basil')
}
assert zone instanceof Zone
println zone.plants*.name
println zone.plants.every { it.zone.is(zone) }          // the parent property was filled in
println zone.bed.soil

def two = builder.zone(name: 'bed') {
    plant(name: 'Carrot', zone: zone)                    // attributes can also reference objects
}
println two.plants[0].zone.name
[Tomato, Basil]
true
peat
bed

CliBuilder

CliBuilder declares a command line’s options and parses the arguments. There are two implementations with the same API: groovy.cli.commons.CliBuilder (based on Apache Commons CLI, whose jar is in the distribution) and groovy.cli.picocli.CliBuilder (based on picocli, with richer usage output and features). The old groovy.util.CliBuilder was removed in Groovy 3.0, so scripts written for Groovy 2.x need the import. The dynamic style defines options by calling methods named after the short option; args, longOpt, type, defaultValue, required, convert and the other named parameters refine them. parse returns an object whose properties are the option values and whose arguments() holds the rest.

import groovy.cli.commons.CliBuilder

def cli = new CliBuilder(usage: 'groovy water [options] zone...', header: 'Waters greenhouse zones:')
cli.m(longOpt: 'minutes', args: 1, argName: 'n', type: int, defaultValue: '5', 'minutes per zone')
cli.d(longOpt: 'dry-run', 'print what would be watered')
cli.h(longOpt: 'help', 'show this help')

def options = cli.parse('-m 10 --dry-run bench bed'.split())
println options.m
println options.d
println options.arguments()

def defaults = cli.parse(['bench'])
println defaults.m                       // default value applies when absent
println defaults.hasOption('m')

cli.usage()
10
true
[bench, bed]
5
false
usage: groovy water [options] zone...
Waters greenhouse zones:
 -d,--dry-run       print what would be watered
 -h,--help          show this help
 -m,--minutes <n>   minutes per zone

The same options can be declared with annotations on an interface (parseFromSpec), on a class (parseFromInstance) or on a script’s variables (@OptionField, @UnparsedField), so the definition lives in a typed class instead of a series of calls:

import groovy.cli.Option
import groovy.cli.Unparsed
import groovy.cli.commons.CliBuilder

interface WaterOptions {
    @Option(shortName = 'm', description = 'minutes per zone', defaultValue = '5') int minutes()
    @Option(shortName = 'd', description = 'print only') boolean dryRun()
    @Unparsed(description = 'zones') List<String> zones()
}

def options = new CliBuilder(usage: 'water').parseFromSpec(WaterOptions, '--minutes 8 -d bench bed'.split())
println options.minutes()
println options.dryRun()
println options.zones()

class WaterConfig {
    @Option(shortName = 'm', description = 'minutes per zone') int minutes = 5
    @Unparsed List<String> zones
}
def config = new WaterConfig()
new CliBuilder().parseFromInstance(config, ['-m', '3', 'bench'] as String[])
println "${config.minutes} ${config.zones}"
8
true
[bench, bed]
3 [bench]

The picocli variant needs the picocli jar on the class path. The jar is not part of the binary distribution, and an @Grab in the script is not enough: CliBuilder is loaded by the groovy launcher’s class loader, which cannot see jars that @Grab adds to the script’s own class loader, and the run fails with a NoClassDefFoundError for a picocli class. Pass the jar with -cp:

groovy -cp picocli-4.7.7.jar water.groovy
import groovy.cli.picocli.CliBuilder

def cli = new CliBuilder(name: 'water', usage: 'water [options] zone...')
cli.m(longOpt: 'minutes', args: 1, type: int, defaultValue: '5', 'minutes per zone')
cli.d(longOpt: 'dry-run', 'print what would be watered')

def options = cli.parse('-m 10 --dry-run bench bed'.split())
println options.m
println options.d
println options.arguments()
cli.usage()
10
true
[bench, bed]
Usage: water [options] zone...
  -d, --dry-run         print what would be watered
  -m, --minutes=PARAM   minutes per zone

Using CliBuilder for a script’s own options is introduced on Running Scripts & the groovy Command.

AntBuilder

AntBuilder runs Ant tasks with builder notation, so a script can copy, zip, compile or fetch files with Ant’s tested tasks and use Groovy for the logic around them. Task names (echo, mkdir, copy), attributes and nested elements (fileset, include) map to Ant’s XML. ant.project.baseDir sets where relative paths resolve, and fileScanner returns the matching files as an iterable. Ant’s own logging prints a line per task; the listener level is lowered here so that only the echo shows. Gradle includes AntBuilder as well (its ant object), see Build Tools: Gradle, Maven & Ant.

import groovy.ant.AntBuilder

def root = File.createTempDir('ant', '')
try {
    new File(root, 'src').mkdirs()
    new File(root, 'src/a.csv').text = 'T1,21.5\n'
    new File(root, 'src/b.csv').text = 'T2,22.0\n'
    new File(root, 'src/notes.txt').text = 'skip me\n'

    def ant = new AntBuilder()
    ant.project.buildListeners.each { it.messageOutputLevel = 1 }   // only errors and warnings (hides [mkdir]/[copy])
    ant.project.baseDir = root                    // resolve relative paths against the temp directory
    ant.sequential {
        echo('copying csv files')
        mkdir(dir: 'backup')
        copy(todir: 'backup') {
            fileset(dir: 'src') { include(name: '*.csv') }
        }
    }
    println new File(root, 'backup').list().sort()

    def scanner = ant.fileScanner {
        fileset(dir: 'src') { include(name: '*.txt') }
    }
    println scanner.collect { it.name }
} finally {
    root.deleteDir()
}
     [echo] copying csv files
[a.csv, b.csv]
[notes.txt]

SwingBuilder

SwingBuilder (module groovy-swing) creates Swing components declaratively: a method name is a component or layout, a string or map gives its text and properties, an actionPerformed closure becomes the listener, and id registers the component as a variable of the builder. A complete user interface also uses edt { ... } to run on the Event Dispatch Thread and frame with show: true, which need a display. The example below builds a panel only, so it runs without one (headless mode is requested first), and presses the button programmatically.

System.setProperty('java.awt.headless', 'true')
import groovy.swing.SwingBuilder

def swing = new SwingBuilder()
def panel = swing.panel(name: 'controls') {
    borderLayout()
    label(text: 'Bench pump', constraints: java.awt.BorderLayout.NORTH)
    button(id: 'start', text: 'Start', constraints: java.awt.BorderLayout.SOUTH,
           actionPerformed: { println 'start pressed' })
}

println panel.getClass().simpleName
println panel.components*.class*.simpleName
println swing.start.text                        // id: registers the component as a builder variable
swing.start.doClick()                           // fires the listener without a screen
JPanel
[JLabel, JButton]
Start
start pressed

SwingBuilder also offers bind for connecting a component property to a model property, for example one marked @Bindable (see AST Transformations: Logging, Safety, Concurrency & Directives).

JmxBuilder

JmxBuilder (module groovy-jmx) exports objects as MBeans, creates connector servers and clients, and defines timers and listeners, all in builder notation. Monitoring a JVM and Tomcat with it is covered on Groovlets, Servlets & JMX. This example exports an object to the platform MBean server and reads it back through the standard JMX API. Attributes exported with '*' are read-only, and JMX capitalises their names.

import groovy.jmx.builder.JmxBuilder
import java.lang.management.ManagementFactory
import javax.management.ObjectName

class Pump {
    int minutes = 5
    boolean running
    void start() { running = true }
}

def pump = new Pump()
def jmx = new JmxBuilder()                       // uses the platform MBeanServer by default
jmx.export {
    bean(target: pump, name: 'greenhouse:type=Pump', attributes: '*', operations: '*')
}

def server = ManagementFactory.platformMBeanServer
def name = new ObjectName('greenhouse:type=Pump')
println server.isRegistered(name)
println server.getMBeanInfo(name).attributes*.name     // JMX capitalises the names
println server.getAttribute(name, 'Minutes')
server.invoke(name, 'start', null, null)
println pump.running
server.unregisterMBean(name)
true
[Minutes, Running]
5
true

Writing your own builder

You can write a builder from scratch with methodMissing as at the start of this page, but the node lifecycle (create, attach to parent, complete) is already implemented by two base classes in groovy.util.

BuilderSupport

Subclass BuilderSupport and override the createNode variants (with and without a map of attributes and a value), setParent and nodeCompleted. createNode is called when a node starts and its return value is the node. nodeCompleted(parent, node) runs when the node’s closure has finished, which is the moment to attach it to its parent, and the builder method returns the completed root node. The example builds a watering plan as maps and lists and adds up the minutes.

// BuilderSupport: override the createNode / nodeCompleted lifecycle hooks.
class WateringPlanBuilder extends BuilderSupport {
    protected createNode(name) { [name: name, steps: []] }
    protected createNode(name, Map attributes) { createNode(name) + attributes }
    protected createNode(name, value) { createNode(name) + [value: value] }
    protected createNode(name, Map attributes, value) { createNode(name, attributes) + [value: value] }

    protected void setParent(parent, child) { }                     // required, nothing to do here

    protected void nodeCompleted(parent, node) {                    // called when a node's closure ends
        if (parent) parent.steps << node
    }
}

int totalMinutes(Map node) {
    if (node.minutes) return node.minutes
    (node.repeat ?: 1) * node.steps.sum(0) { totalMinutes(it) }
}

def plan = new WateringPlanBuilder().morning {
    bench(repeat: 2) {
        mist(minutes: 3)
        soak(minutes: 10)
    }
    bed {
        drip(minutes: 30)
    }
}

println plan.name
println plan.steps*.name
println totalMinutes(plan)
morning
[bench, bed]
56

The total is 2 * (3 + 10) for bench plus 30 for bed.

FactoryBuilderSupport

FactoryBuilderSupport keeps one Factory per node name instead of one createNode for all. A factory creates the instance (newInstance), by default copies the attributes onto its properties, and attaches a child to its parent (setChild). registerFactory(name, factory) and registerBeanFactory(name, class) register them. This is how SwingBuilder, ObjectGraphBuilder and JmxBuilder are built. A bean factory (registerBeanFactory) only creates the instances and sets properties, it does not attach children, so a parent-child relation needs a factory that overrides setChild, as here:

class Zone {
    String name
    List<Plant> plants = []
    void addPlant(Plant p) { plants << p }
}
class Plant {
    String name
    int waterMinutes
}

// A factory creates a node and attaches its children to it.
class BeanFactory extends AbstractFactory {
    Class type

    Object newInstance(FactoryBuilderSupport builder, Object name, Object value, Map attributes) {
        type.getDeclaredConstructor().newInstance()
    }

    void setChild(FactoryBuilderSupport builder, Object parent, Object child) {
        parent."add${child.getClass().simpleName}"(child)
    }
}

class GreenhouseBuilder extends FactoryBuilderSupport {
    GreenhouseBuilder() {
        registerFactory('zone', new BeanFactory(type: Zone))
        registerFactory('plant', new BeanFactory(type: Plant))   // attributes become properties
    }
}

def zone = new GreenhouseBuilder().zone(name: 'bench') {
    plant(name: 'Tomato', waterMinutes: 10)
    plant(name: 'Basil', waterMinutes: 4)
}

println zone.getClass().simpleName
println zone.plants*.name
println zone.plants.sum { it.waterMinutes }
Zone
[Tomato, Basil]
14

Checked builders with @DelegatesTo

A builder does not have to extend anything. A method that takes a closure, sets a delegate and a resolve strategy, and runs it is already a small builder, and annotating the closure parameter with @DelegatesTo tells the IDE and the type checker which methods the closure body may call. With @CompileStatic the structure is then checked at compile time.

import groovy.transform.CompileStatic

@CompileStatic
class ZoneSpec {
    String name
    List<String> plants = []
    int minutes = 5

    void plant(String plantName) { plants << plantName }
    void water(int forMinutes) { minutes = forMinutes }
}

@CompileStatic
ZoneSpec zone(String name, @DelegatesTo(value = ZoneSpec, strategy = Closure.DELEGATE_FIRST) Closure body) {
    def spec = new ZoneSpec(name: name)
    body.delegate = spec
    body.resolveStrategy = Closure.DELEGATE_FIRST
    body()
    spec
}

@CompileStatic
def build() {
    zone('bench') {
        plant 'Tomato'
        plant 'Basil'
        water 10
        // plantt 'typo'    // would not compile: the checker knows the delegate
    }
}

def spec = build()
println "${spec.name}: ${spec.plants}, ${spec.minutes} min"
bench: [Tomato, Basil], 10 min

A misspelt call is now a compile error instead of a run-time MissingMethodException. With plantt 'typo' in the block:

b15b.groovy: 28: [Static type checking] - Cannot find matching method b15b#plantt(java.lang.String). Please check if the declared type is correct and if the method exists.
 @ line 28, column 9.
           plantt 'typo'
           ^

The mechanism is explained on Closure Delegation: Owner, Delegate & This, and the full treatment of @DelegatesTo and DSL design is on Domain Specific Languages. See Static Type Checking & @CompileStatic for the checker.

Where the book is outdated

The Manning book Groovy in Action (first edition, Groovy 1.0, 2007) has a chapter on builders, and the pattern it explains (method names as nodes, closures as children, NodeBuilder, MarkupBuilder, AntBuilder, SwingBuilder) is still how Groovy builders work. Check these points against the current documentation:

  • Builders now live in modules: XML builders in groovy-xml, SwingBuilder in groovy-swing, AntBuilder in groovy-ant (package groovy.ant), JmxBuilder in groovy-jmx. The binary distribution contains them all, but a Gradle or Maven project must declare them.

  • JsonBuilder, StreamingJsonBuilder and FileTreeBuilder did not exist in 1.0, and the book predates the picocli and commons CliBuilder implementations (groovy.util.CliBuilder was removed in 3.0).

  • StreamingMarkupBuilder, DOMBuilder and the SAX and StAX builders are described in the current documentation with different (and shorter) examples than the book’s.

  • Type-checked builders with @DelegatesTo and @CompileStatic are a later addition to the language.

  • Baselines: Groovy 6.0 needs JDK 17 or later.

Common mistakes

  • Writing a builder closure and expecting local variables to be shadowed by the delegate: the closure’s own locals always win, but the delegate is consulted before the owner only with DELEGATE_FIRST. See Closure Delegation: Owner, Delegate & This.

  • Using registerBeanFactory and expecting children to be added to the parent: it only creates the instance and sets the attributes.

  • Calling SwingBuilder frame methods on a machine without a display: java.awt.HeadlessException. Build the panels, not the frame, in tests.

  • Relying on @Grab for the picocli jar; use -cp.

  • Using a node name that is also a real method of the builder or of Object (such as hashCode or toString): the real method is called instead of the builder interception.