Skip to content

Groovy 6 compatibility: changes required in Grails, and when each can be removed #16157

Description

@jamesfredley

Tracking issue for the changes Apache Grails needs in order to build, test, and ship against Apache Groovy 6. It documents what landed via #15558 on 9.0.x, why each item exists, how it affects applications upgrading from Grails 7/8, and the condition under which it can be deleted.

Current state: Groovy 6.0.0-beta-2, Grails 9 (9.0.x), JDK 21, Spring Boot 4.1, Spock 2.4-groovy-5.0 (no groovy-6.0 artifact published yet).

Every row was established empirically: a workaround was only kept when removing it produced an observed failure, and only removed when the build and tests stayed green without it.

Documentation standard (treat every workaround the same)

Each remaining workaround must have all five of:

  1. A source comment (or build-file comment) naming Groovy 6 / the specific trigger.
  2. A pointer to this issue (#16157).
  3. The observed failure without the change.
  4. The deletion condition ("remove when X").
  5. An end-user impact class from the table below.

Do not leave "framework-only" items uncommented while test-only or build-only items are documented, or the reverse.

Impact classes

Class Meaning
App source An upgraded application can hit the same Groovy 6 behaviour in its own code and may need the same change
App runtime Framework behaviour the app calls at runtime changed; no app source edit required if they stay on public APIs
App test build The app's test compile/runtime needs a Gradle/Maven flag or extra dependency until an upstream artifact exists
Framework only Confined to Grails internals / this repo's build. Apps do not copy it
Permanent Not a workaround. Keep it

1. Upstream Groovy 6 regression (not filed upstream)

Static type checking merges the flow state of a || inside a closure to void, so a variable guarded by x == null || ... becomes unusable in the branch body.

@groovy.transform.CompileStatic
class Repro {
    void bind(Collection val, Class componentType) {
        List boundItems = []
        ((Collection) val).each { item ->
            if (item == null || componentType.isAssignableFrom(item.getClass())) {
                boundItems << item   // FAILS on 6.0.0-beta-2
            }
        }
    }
}
[Static type checking] - Cannot find matching method java.util.ArrayList#leftShift(void)

The Map form fails equivalently with Cannot find matching method java.util.LinkedHashMap#putAt(java.lang.Object, void).

  • Compiles cleanly on Groovy 5.0.8; fails on 6.0.0-beta-2.
  • && in the same position compiles. Only || triggers it.
  • Workaround: hoist the disjunction into a boolean local. Short-circuiting and null handling are unchanged.

Applied in grails-web-databinding/.../GrailsWebDataBinder.groovy (Collection and Map branches).

Upgrade impact: App source. Any @CompileStatic / @GrailsCompileStatic closure in an application that uses || as a type guard can fail to compile on Groovy 6 until hoisted the same way. The Grails framework change does not alter runtime bind behaviour.

2. Remaining items on 6.0.0-beta-2

# File Groovy 6 behaviour Observed failure without it Removable when End-user impact
1 grails-web-databinding/.../GrailsWebDataBinder.groovy || flow state inside a closure infers as void ArrayList#leftShift(void) Section 1 is fixed upstream App source if the app has the same @CompileStatic pattern. Framework bind behaviour unchanged
2 grails-core/.../config/external/WriterFilteringMap.groovy @Delegate now generates mutator methods that were previously excluded WriteFilteringMapSpec: getWrittenValues().size() == 0 Groovy restores the previous @Delegate mutator exclusion App source if the app uses @Delegate the same way. Grails config filtering stays correct because of this change
3 grails-datamapping-core/.../GormEntity.groovy Generic trait method signatures are specialized differently, so trait-injected merge(Object) is not found NoSuchMethodException: Book.merge(Object) Groovy restores prior generic trait-signature resolution App runtime. Domain merge() is a public GORM API. Without this, upgraded apps can fail at runtime when calling merge
4 grails-datastore-core/.../reflect/ClassPropertyFetcher.java Interface methods surface differently during property introspection MissingMethodException ... __transients$get Unknown - needs upstream confirmation App runtime. Property / transients introspection is used by GORM and databinding. Apps should not call this class directly
5 grails-testing-support-http-client/.../utils/XmlUtils.groovy SAX/JAXP feature set recognised by the default parser changed DOCTYPE is disallowed... Unknown. This is XXE-hardening; do not drop it for convenience App test build if the app uses this testing helper. Not a production runtime change. Keep the hardening
6 grails-validation/.../ValidateableTraitSpec.groovy Static trait methods are emitted with a modifier combination the verifier rejects illegal combination of modifiers: abstract and static Groovy fixes static trait-method emission App source. Validateable is a public trait. Command objects / domain-like types in the app can fail class verification on Groovy 6
7 grails-data-hibernate7/.../HibernateGormInstanceApi.groovy Negated instanceof requires explicit parenthesization Compile failure Groovy restores prior parsing precedence App source if the app has !(x instanceof Foo) under static compile. Framework persistence behaviour unchanged
8 grails-fields/.../BeanPropertyAccessorImpl.groovy @Canonical no longer implies @MapConstructor Compile failure Groovy reinstates implied @MapConstructor, or Grails keeps the explicit annotation permanently App source. Apps using @Canonical and expecting a map constructor must add @MapConstructor themselves on Groovy 6
9 grails-views-gson/.../GrailsJsonViewHelper.groovy, .../internal/TemplateRenderer.groovy Closure/generic inference under @CompileStatic Compile failure Unknown Framework only unless the app copies these helpers. Gson views keep compiling because of this
10 grails-testing-support-http-client/.../HttpClientSupport.groovy A static final constant in a Spec collides with a trait instance getter cannot have both a static and an instance method Groovy restores prior static/instance resolution App test build. Specs that mix a static FORM (or similar) with this trait will fail to compile. Rename the constant or drop the static
11 gradle/*-test-config.gradle, gradle.properties, CompilePlugin, forge template Spock 2.4 is built against Groovy 5 and refuses to run on Groovy 6 IncompatibleGroovyVersionException / SpockTransform Spock ships 2.4-groovy-6.0 (not on Maven Central as of 2026-08-18; latest is 2.4-groovy-5.0. Spock master compiles against Groovy 6.0.0-alpha-2 only) App test build. Any app or plugin that compiles/tests with Spock on Groovy 6 must pass -Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true until Spock publishes a groovy-6 artifact. This is the largest end-user item
12 grails-common/build.gradle, dependencies.gradle groovy-callsite is a separate module in Groovy 6 Missing class at runtime when indy is off Never Permanent. Apps/plugins that compile with indy = false need groovy-callsite on the compile/runtime classpath
13 build-logic/.../SbomPlugin.groovy Groovy 6 pulls JLine 4, whose POM omits the licence SBOM validation expects SBOM licence validation failure JLine publishes complete licence metadata Framework only (Grails repo / SBOM). Not an application concern
14 settings.gradle Micronaut island is pinned to Groovy 5 Version conflict if included The Micronaut island supports Groovy 6 (#16161) App runtime / BOM. Groovy 6 snapshots omit grails-micronaut-*. Apps that depend on Micronaut integration cannot use those artifacts from this line yet
15 .github/workflows/groovy-joint-workflow.yml Groovy 6 development is still on master; GROOVY_6_0_X does not exist Joint validation checks out a missing branch Apache Groovy cuts GROOVY_6_0_X Framework only (Grails CI)

3. Upgrade checklist for application authors (7.x / 8.x -> 9 on Groovy 6)

These are the items an application actually has to do or know. Everything else in the table is absorbed inside Grails.

  1. Spock. Until org.spockframework:spock-core:2.4-groovy-6.0 exists, add the version-check opt-out on Groovy compile and test JVMs (-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true). Without it, grails-test-core and any Spec will fail to compile. This is unsupported by Spock and may produce odd runtime errors.
  2. groovy-callsite. If the app keeps grails { indy = false } (the current plugin default), add org.apache.groovy:groovy-callsite or inherit it from the Grails BOM.
  3. @CompileStatic || guards inside closures. Hoist to a boolean local. Same as item 1 in the table.
  4. Validateable / static trait methods. If verification fails with abstract and static, that is Groovy 6 trait emission, not a Grails API break.
  5. @Canonical map constructors. Add @MapConstructor explicitly.
  6. !(x instanceof T) under static compile: parenthesize !(x instanceof T).
  7. Micronaut. grails-micronaut-bom and the island are not in Groovy 6 snapshots (#16161).
  8. Do not drop XmlUtils XXE hardening if you copied that helper.

GORM merge(), transients introspection, and config filtering are handled inside Grails. Upgraded apps that stay on public APIs should not need source edits for those.

4. Workarounds retired at 6.0.0-beta-2

Workaround Why it is gone
Gradle 9.6.1 bump Never a Groovy 6 requirement
CoreGrailsPlugin BeanConfiguration.addProperty Obsolete after the beanRegistrar rewrite
GrailsApplicationLifeCycle rewritten as a Java interface Groovy 6 handles the default method again. Verified under -PgrailsIndy=false
~30 further snapshot-era compile workarounds No longer reproduce on 6.0.0-beta-2

5. Not Groovy 6

  • ConfigurationBuilder nested-map handling is Spring 7 and lives on 8.0.x via #16160 / #16159. It is not a Groovy 6 workaround.
  • Embedded MongoDB / Testcontainers failures seen locally are environmental.

Verification

#15558 merged to 9.0.x on 2026-08-18. Snapshot publish is the 9.0.x push CI publish job.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions