You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ma...@apache.org on 2026/09/24 08:54:42 UTC

(grails-core) 01/03: Upgrade Groovy to 6.0.0

This is an automated email from the ASF dual-hosted git repository.

matrei pushed a commit to branch deps/groovy-6.0.0
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit d1cbf7fd0a822dd4791cc5869d4af594705e210a
Author: Mattias Reichel <ma...@gmail.com>
AuthorDate: Wed Sep 23 22:57:03 2026 +0200

    Upgrade Groovy to 6.0.0
    
    Groovy 6 ships the classic (non-invokedynamic) call-site runtime as the
    separate groovy-callsite module, and its compiler refuses to generate that
    bytecode without it on the compile classpath. Grails 8 compiles with indy
    off, so grails-common exports the module and the root build adds it to
    every source set of the modules below grails-common.
    
    Spock has no groovy-6.0 build, and 2.4-groovy-5.0 checks the Groovy major
    both in its compiler AST transform and when a Spec starts. The opt-out
    system property is set on the forked Groovy compiler and Test tasks by
    CompilePlugin, on groovydoc runs, in the end-to-end build, and for
    applications by the Grails Gradle plugin on GroovyCompile, Test and the
    forked view compiler. A build that sets its own value keeps it.
    
    Groovy 6 moves groovysh to JLine 4, so jline.version follows and the
    SBOM plugin corrects the JLine 4 licence metadata by pattern.
    
    Source changes for Groovy 6.0.0: parameterized types and type variables
    are no longer class literals; `!x instanceof T` is rejected (it was always
    false); a statically compiled map-style constructor call needs an
    explicit @MapConstructor; @Delegate generates over declared overrides
    (config write filtering, dirty-checking reversed views) and no longer
    satisfies the abstract-method check for the JSON view helper's render
    overloads; the stub generator writes a precompiled trait's static
    methods as `static abstract`; and generic trait methods are specialized
    on the implementing class. Each workaround is commented with its
    trigger, failure, deletion condition and impact class as catalogued in
    issue #16157 on the 9.0.x line.
    
    The upgrade guide gains a Groovy 6 section covering the call-site module,
    the Spock opt-out, JLine 4 and the application-source changes.
---
 .agents/skills/gradle-developer/SKILL.md           |  8 ++--
 .agents/skills/grails-developer/SKILL.md           |  4 +-
 .agents/skills/groovy-developer/SKILL.md           |  6 +--
 AGENTS.md                                          |  6 +--
 .../apache/grails/buildsrc/CompilePlugin.groovy    | 27 +++++++++---
 .../apache/grails/buildsrc/GroovydocRunner.groovy  |  4 ++
 .../org/apache/grails/buildsrc/SbomPlugin.groovy   | 33 +++++++-------
 .../grails/buildsrc/CompilePluginSpec.groovy       | 11 +++++
 .../apache/grails/buildsrc/SbomPluginSpec.groovy   | 28 ++++++++++++
 build.gradle                                       | 17 ++++++++
 dependencies.gradle                                | 12 ++++-
 end-to-end/README.md                               |  4 +-
 end-to-end/build.gradle                            | 11 +++++
 end-to-end/legacy-g7-command-plugin/build.gradle   |  2 +-
 end-to-end/settings.gradle                         |  2 +-
 gradle/mongodb-forked-test-config.gradle           |  4 +-
 gradle/mongodb-test-config.gradle                  |  4 +-
 grails-common/build.gradle                         |  7 +++
 .../main/groovy/grails/boot/GrailsBanner.groovy    |  3 +-
 .../config/external/WriterFilteringMap.groovy      |  5 ++-
 .../orm/hibernate/HibernateGormInstanceApi.groovy  |  4 +-
 .../hibernate/HibernateGormInstanceApiSpec.groovy  | 19 ++++++++
 .../compiler/gorm/GormEntityTransformSpec.groovy   | 22 +++++++++-
 .../dirty/checking/DirtyCheckingList.groovy        |  5 ++-
 .../dirty/checking/DirtyCheckingSortedSet.groovy   |  3 +-
 .../src/en/guide/upgrading/upgrading80x.adoc       | 51 +++++++++++++++++++++-
 .../formfields/BeanPropertyAccessorImpl.groovy     |  6 +++
 .../formfields/BeanPropertyAccessorImplSpec.groovy | 51 ++++++++++++++++++++++
 .../gradle/plugin/core/GrailsGradlePlugin.groovy   | 17 ++++++++
 .../views/AbstractGroovyTemplateCompileTask.groovy | 11 +++++
 .../core/GrailsGradlePluginToolchainSpec.groovy    |  9 ++++
 .../fork-settings-defaults/build.gradle            | 19 ++++++++
 .../Sitemesh3RenderViewMutatorSpec.groovy          |  2 +-
 .../oauth2/SpringSecurityOauth2BaseService.groovy  |  3 +-
 .../testing/ParameterizedGrailsUnitTest.groovy     |  3 +-
 .../mongo/StartMongoGrailsUnitExtension.groovy     |  3 +-
 .../grails/validation/ValidateableTraitSpec.groovy | 13 ++++++
 .../json/view/api/GrailsJsonViewHelper.groovy      | 26 ++++++++---
 .../json/view/api/internal/TemplateRenderer.groovy | 35 +++++++++++++++
 .../plugin/json/view/TemplateRendererSpec.groovy   | 23 ++++++++++
 40 files changed, 466 insertions(+), 57 deletions(-)

diff --git a/.agents/skills/gradle-developer/SKILL.md b/.agents/skills/gradle-developer/SKILL.md
index 98a092170c..2c7196f5d4 100644
--- a/.agents/skills/gradle-developer/SKILL.md
+++ b/.agents/skills/gradle-developer/SKILL.md
@@ -138,10 +138,10 @@ Presence-based flags (property **present**, value optional) match `skipFunctiona
 
 Current line: **Gradle 9.7.1** (`distributionUrl` + `gradleToolingApiVersion=9.7.1`). Upstream may already ship a newer 9.7.x patch - this repo rides close to latest **only after** a deliberate multi-location bump PR. Do not "helpfully" jump one wrapper ahead of the rest.
 
-**Two Groovy stacks:** Gradle itself embeds **Groovy 4** for build logic. Application/runtime code on 8.0.x is **Groovy 5**. That is why `dependencies.gradle` keeps separate maps:
+**Two Groovy stacks:** Gradle itself embeds **Groovy 4** for build logic. Application/runtime code on 8.0.x is **Groovy 6**. That is why `dependencies.gradle` keeps separate maps:
 
 - `gradleBomDependencyVersions` / `gradle-groovy.version` / `gradle-spock.version` → build tooling (Groovy 4 / Spock groovy-4)
-- `bomDependencyVersions` / `groovy.version` / `spock.version` → apps and framework modules (Groovy 5 / Spock groovy-5.0)
+- `bomDependencyVersions` / `groovy.version` / `spock.version` → apps and framework modules (Groovy 6 / Spock groovy-5.0, which runs on Groovy 6 with its version check switched off until Spock ships a groovy-6.0 build)
 
 Never unify those casually.
 
@@ -293,7 +293,7 @@ For POM property generation, **map key must be the dependency name prefix**:
 
 ```groovy
 bomDependencyVersions = [
-    'groovy.version': '5.1.3',
+    'groovy.version': '6.0.0',
 ]
 bomDependencies = [
     'groovy': "org.apache.groovy:groovy:${bomDependencyVersions['groovy.version']}",
@@ -596,7 +596,7 @@ Develocity: `https://develocity.apache.org` - build scans publish when authentic
 | `includeBuild` the root into `end-to-end` | Publish to `build/local-maven` via `publishAllPublicationsToTestCaseMavenRepoRepository` |
 | Use `publishAllToMavenLocal` to feed end-to-end | That fills `~/.m2`; end-to-end reads `<repo>/build/local-maven` |
 | Bump `legacy-g7-command-plugin` wrapper to Gradle 9 | Leave it on the Grails 7-pinned Gradle 8.x |
-| Mix Gradle-embedded Groovy 4 pins into app Groovy 5 BOM | Keep `gradleBom*` vs `bom*` maps separate |
+| Mix Gradle-embedded Groovy 4 pins into app Groovy 6 BOM | Keep `gradleBom*` vs `bom*` maps separate |
 
 ---
 
diff --git a/.agents/skills/grails-developer/SKILL.md b/.agents/skills/grails-developer/SKILL.md
index 8e83754866..ddc980310b 100644
--- a/.agents/skills/grails-developer/SKILL.md
+++ b/.agents/skills/grails-developer/SKILL.md
@@ -33,7 +33,7 @@ Activate this skill when developing with current Grails, including:
 Current Grails is built on:
 - **Spring Boot**: 4.1.x
 - **Spring Framework**: 7.0.x
-- **Groovy**: 5.1.x
+- **Groovy**: 6.0.x
 - **Gradle**: 9.6.x
 - **Spock**: 2.4-groovy-5.0
 - **Jakarta EE**: 10 (migrated from javax.*)
@@ -865,6 +865,6 @@ grails.plugin.springsecurity.controllerAnnotations.staticRules = [
 - **Grails User Guide**: https://grails.apache.org/docs/latest/guide/single.html
 - **GORM Documentation**: https://grails.apache.org/docs/latest/grails-data/
 - **Grails Plugins**: https://grails.apache.org/plugins.html
-- **Groovy 5 Documentation**: https://groovy-lang.org/documentation.html#all-versions (select latest 5.1.x)
+- **Groovy 6 Documentation**: https://groovy-lang.org/documentation.html#all-versions (select latest 6.0.x)
 - **Spock Framework**: https://spockframework.org/spock/docs/2.4/all_in_one.html
 - **Geb Manual**: https://groovy.apache.org/geb/manual/current/
diff --git a/.agents/skills/groovy-developer/SKILL.md b/.agents/skills/groovy-developer/SKILL.md
index 2b370502be..2af957fa56 100644
--- a/.agents/skills/groovy-developer/SKILL.md
+++ b/.agents/skills/groovy-developer/SKILL.md
@@ -1,6 +1,6 @@
 ---
 name: groovy-developer
-description: Expert guide for Groovy 5 development, covering concise syntax, closures, DSLs, metaprogramming, static compilation, and integration with Java 21 and Grails
+description: Expert guide for Groovy 6 development, covering concise syntax, closures, DSLs, metaprogramming, static compilation, and integration with Java 21 and Grails
 license: Apache-2.0
 ---
 <!--
@@ -11,7 +11,7 @@ Licensed to the Apache Software Foundation (ASF) under one or more contributor l
 
 ## What I Do
 
-- Provide deep expertise in Groovy 5.1, including core language features: syntax, closures, traits, GStrings, operators, and Groovy Truth.
+- Provide deep expertise in Groovy 6.0, including core language features: syntax, closures, traits, GStrings, operators, and Groovy Truth.
 - Guide metaprogramming techniques: runtime (categories, ExpandoMetaClass, extension modules) and compile-time (AST transformations like @Immutable, @Builder, @Delegate, @Singleton, @Log, @Memoized).
 - Assist with DSL creation and usage (MarkupBuilder, JsonBuilder, builders for configuration).
 - Support testing with Spock Framework (BDD, data-driven tests, mocking) and built-in power assertions.
@@ -509,7 +509,7 @@ sql.execute("INSERT INTO books (title) VALUES ($title)")
 
 ## Resources
 
-- **Groovy 5 Documentation**: https://groovy-lang.org/documentation.html#all-versions (select latest 5.1.x)
+- **Groovy 6 Documentation**: https://groovy-lang.org/documentation.html#all-versions (select latest 6.0.x)
 - **Groovy Style Guide**: https://groovy-lang.org/style-guide.html
 - **GORM Documentation**: https://grails.apache.org/docs/latest/grails-data/
 - **Spock Framework**: https://spockframework.org/spock/docs/2.4/all_in_one.html
diff --git a/AGENTS.md b/AGENTS.md
index 6c7eaabb4e..12abde794e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -74,7 +74,7 @@ export GRADLE_OPTS="-Dorg.gradle.jvmargs=-Xmx4G"
 | Skill | Path | Use For |
 |-------|------|---------|
 | **grails-developer** | `.agents/skills/grails-developer/SKILL.md` | Current Grails apps, GORM, controllers, views |
-| **groovy-developer** | `.agents/skills/groovy-developer/SKILL.md` | Groovy 5 syntax, closures, DSLs, Spock |
+| **groovy-developer** | `.agents/skills/groovy-developer/SKILL.md` | Groovy 6 syntax, closures, DSLs, Spock |
 | **gradle-developer** | `.agents/skills/gradle-developer/SKILL.md` | Gradle 9 builds, BOM/platforms, convention plugins, wrappers |
 | **grails-8-upgrade** | `.agents/skills/grails-8-upgrade/SKILL.md` | Upgrading Grails applications from 7.x to 8 |
 | **java-developer** | `.agents/skills/java-developer/SKILL.md` | Java 21 features, Groovy interop |
@@ -88,7 +88,7 @@ export GRADLE_OPTS="-Dorg.gradle.jvmargs=-Xmx4G"
 | Component | Version |
 |-----------|---------|
 | JDK | 21+ (baseline 21) |
-| Groovy | 5.1.x |
+| Groovy | 6.0.x |
 | Spring Boot | 4.1.x |
 | Spring Framework | 7.0.x |
 | Spock | 2.4-groovy-5.0 |
@@ -309,7 +309,7 @@ and known non-findings — before reporting issues.
 ## Resources
 
 - **Grails Guide**: https://grails.apache.org/docs/latest/guide/single.html
-- **Groovy 5 Docs**: https://groovy-lang.org/documentation.html#all-versions (select latest 5.1.x)
+- **Groovy 6 Docs**: https://groovy-lang.org/documentation.html#all-versions (select latest 6.0.x)
 - **Spock 2.4 Docs**: https://spockframework.org/spock/docs/2.4/all_in_one.html
 - **GORM Docs**: https://grails.apache.org/docs/latest/grails-data/
 - **Issues**: https://github.com/apache/grails-core/issues
diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy
index 935185516f..c61250d830 100644
--- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy
+++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy
@@ -34,6 +34,7 @@ import org.gradle.api.tasks.bundling.Jar
 import org.gradle.api.tasks.compile.GroovyCompile
 import org.gradle.api.tasks.compile.JavaCompile
 import org.gradle.api.tasks.javadoc.Javadoc
+import org.gradle.api.tasks.testing.Test
 import org.gradle.external.javadoc.StandardJavadocDocletOptions
 
 import static org.apache.grails.buildsrc.GradleUtils.lookupProperty
@@ -118,11 +119,13 @@ class CompilePlugin implements Plugin<Project> {
                 if (!it.options.compilerArgs.contains('-parameters')) {
                     it.options.compilerArgs.add('-parameters')
                 }
-                // Grails 8 keeps invokedynamic off for published artifacts. Groovy 5's
-                // compiler default is indy=true, which is a large runtime regression for
-                // dynamic Groovy (see #15293). Unpublished build-logic uses Gradle's
-                // default. Grails 9 / Groovy 6 can flip this. CI can still opt in with
-                // -PgrailsIndy=true (same property as grails-extension-gradle-config.gradle).
+                // Grails 8 keeps invokedynamic off for published artifacts. The Groovy
+                // compiler default is indy=true, which was a large runtime regression for
+                // dynamic Groovy on Groovy 5 (see #15293). Unpublished build-logic uses
+                // Gradle's default. On Groovy 6 classes compiled this way need the
+                // groovy-callsite module at runtime, which grails-common carries. CI can
+                // still opt in with -PgrailsIndy=true (same property as
+                // grails-extension-gradle-config.gradle).
                 it.groovyOptions.optimizationOptions.put('indy', lookupProperty(project, 'grailsIndy', false))
                 // encoding needs to be the same since it's different across platforms
                 it.options.encoding = StandardCharsets.UTF_8.name()
@@ -130,6 +133,13 @@ class CompilePlugin implements Plugin<Project> {
                 // always set an isolated build to ensure grails.factories aren't accidentally merged since every project
                 // in this mono repo should be an isolated projected
                 it.options.forkOptions.jvmArgs = ['-Xms128M', '-Xmx2G', '-Dgrails.isolated.build=true']
+                // Groovy 6 with Spock 2.4-groovy-5.0 (#16157): Spock's global AST transform is on
+                // every compile classpath that carries spock-core and checks the Groovy major
+                // version inside the forked compiler, failing with IncompatibleGroovyVersionException.
+                // Remove when the BOM moves to a Spock build for Groovy 6. App test build: an
+                // application's build gets the same flag from the Grails Gradle plugin.
+                it.groovyOptions.forkOptions.jvmArgs = (it.groovyOptions.forkOptions.jvmArgs ?: []) +
+                        ['-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true']
                 // Publish THIS project's base.dir to the forked Groovy compiler. Gradle reuses a forked
                 // compiler daemon for a task whose requested fork arguments the daemon already satisfies,
                 // so a compile that does NOT request base.dir can be handed a daemon started for another
@@ -150,6 +160,13 @@ class CompilePlugin implements Plugin<Project> {
                 it.groovyOptions.configurationScript =
                         GradleUtils.findRootGrailsCoreDir(project).file('gradle/groovy-compile-configscript.groovy').asFile
             }
+            project.tasks.withType(Test).configureEach {
+                // Groovy 6 with Spock 2.4-groovy-5.0 (#16157): the same check runs when a Spec
+                // starts, and again whenever a test compiles Groovy at runtime with spock-core on
+                // the classpath. A system property survives a later `jvmArgs = [...]` assignment.
+                // Remove with the compiler flag above.
+                it.systemProperty('spock.iKnowWhatImDoing.disableGroovyVersionCheck', 'true')
+            }
             project.tasks.named('compileGroovy', GroovyCompile).configure { GroovyCompile task ->
                 // Resource-only changes do not ordinarily invalidate compilation. This file changes
                 // whether the compiler owns the generated imports resource, so adding or deleting it
diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocRunner.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocRunner.groovy
index 2f8d9da047..1918b69315 100644
--- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocRunner.groovy
+++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GroovydocRunner.groovy
@@ -83,6 +83,10 @@ abstract class GroovydocRunner {
             spec.classpath = classpath
             spec.mainClass.set('groovy.ui.GroovyMain')
             spec.args = [scriptFile.absolutePath, paramsFile.absolutePath]
+            // Groovy 6 with Spock 2.4-groovy-5.0 (#16157): the documentation classpath can carry
+            // spock-core, whose global AST transform refuses to load on Groovy 6 without this.
+            // Remove when the BOM moves to a Spock build for Groovy 6. Framework only.
+            spec.systemProperty('spock.iKnowWhatImDoing.disableGroovyVersionCheck', 'true')
             spec.maxHeapSize = maxHeapSize
         }
     }
diff --git a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy
index 9bbeca3087..2bb7e9ab98 100644
--- a/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy
+++ b/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/SbomPlugin.groovy
@@ -113,20 +113,9 @@ class SbomPlugin implements Plugin<Project> {
             'pkg:maven/de.bwaldvogel/mongo-java-server@1.47.0?type=jar'       : 'BSD-3-Clause',
             'pkg:maven/de.bwaldvogel/mongo-java-server-core@1.47.0?type=jar'  : 'BSD-3-Clause',
             'pkg:maven/de.bwaldvogel/mongo-java-server-memory-backend@1.47.0?type=jar': 'BSD-3-Clause',
-            // The whole org.jline group declares "The BSD License", which maps incorrectly because of
+            // The org.jline group is handled by forcedLicenseFor below: every artifact of it declares
+            // "The BSD License", which maps incorrectly because of
             // https://github.com/CycloneDX/cyclonedx-core-java/issues/205 - the POMs point at BSD-3-Clause.
-            // jline.version tracks the JLine version Groovy ships, so every module resolves to one version.
-            'pkg:maven/org.jline/jansi@3.30.16?type=jar'                       : 'BSD-3-Clause',
-            'pkg:maven/org.jline/jline@3.30.16?type=jar'                       : 'BSD-3-Clause',
-            'pkg:maven/org.jline/jline-builtins@3.30.16?type=jar'              : 'BSD-3-Clause',
-            'pkg:maven/org.jline/jline-console@3.30.16?type=jar'               : 'BSD-3-Clause',
-            'pkg:maven/org.jline/jline-native@3.30.16?type=jar'                : 'BSD-3-Clause',
-            'pkg:maven/org.jline/jline-reader@3.30.16?type=jar'                : 'BSD-3-Clause',
-            'pkg:maven/org.jline/jline-style@3.30.16?type=jar'                 : 'BSD-3-Clause',
-            'pkg:maven/org.jline/jline-terminal@3.30.16?type=jar'              : 'BSD-3-Clause',
-            'pkg:maven/org.jline/jline-terminal-jansi@3.30.16?type=jar'        : 'BSD-3-Clause',
-            'pkg:maven/org.jline/jline-terminal-jna@3.30.16?type=jar'          : 'BSD-3-Clause',
-            'pkg:maven/org.jline/jline-terminal-jni@3.30.16?type=jar'          : 'BSD-3-Clause',
             'pkg:maven/org.jruby/jzlib@1.1.5?type=jar'                        : 'BSD-3-Clause', // https://web.archive.org/web/20240822213507/http://www.jcraft.com/jzlib/LICENSE.txt shows it's a 3 clause
             'pkg:maven/org.liquibase.ext/liquibase-hibernate5@4.27.0?type=jar': 'Apache-2.0', // maps incorrectly because of https://github.com/liquibase/liquibase/issues/2445 & the base pom does not define a license
             'pkg:maven/org.json/json@20251224?type=jar'                       : 'Public-Domain', // required due to jedis, https://issues.apache.org/jira/browse/LEGAL-666 approves this usage
@@ -453,6 +442,20 @@ class SbomPlugin implements Plugin<Project> {
         }
     }
 
+    private static String forcedLicenseFor(String bomRef) {
+        if (LICENSE_MAPPING.containsKey(bomRef)) {
+            return LICENSE_MAPPING[bomRef]
+        }
+        // Groovy 6 ships on JLine 4 (#16157), and jline.version follows it. JLine's LICENSE.txt is
+        // BSD-3-Clause, but CycloneDX reads "The BSD License" as BSD-4-Clause
+        // (cyclonedx-core-java#205). Limited to the org.jline Maven jar family at 4.x so a bump
+        // inside that line needs no new entry. Framework only.
+        if (bomRef.matches('pkg:maven/org\\.jline/[^/@?]+@4\\.[^?]+\\?type=jar')) {
+            return 'BSD-3-Clause'
+        }
+        null
+    }
+
     /**
      * Picks the most appropriate license for a dependency from a list of license choices.
      * This method is called at execution time and should not access Task.project.
@@ -471,10 +474,10 @@ class SbomPlugin implements Plugin<Project> {
         }
 
         logger.info('Picking license for {} from {} choices', bomRef, licenseChoices.size())
-        if (LICENSE_MAPPING.containsKey(bomRef)) {
+        def licenseId = forcedLicenseFor(bomRef)
+        if (licenseId) {
             // There are several reasons that cyclone will get the license wrong, usually due to upstream not publishing information or publishing it incorrectly
             // see the licenseMapping map above for details
-            def licenseId = LICENSE_MAPPING[bomRef]
             logger.info('Forcing license for {} to {}', bomRef, licenseId)
 
             def licenseBlock = LICENSES[licenseId]
diff --git a/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/CompilePluginSpec.groovy b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/CompilePluginSpec.groovy
index 83721b2219..1238fd1a13 100644
--- a/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/CompilePluginSpec.groovy
+++ b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/CompilePluginSpec.groovy
@@ -83,6 +83,8 @@ class CompilePluginSpec extends Specification {
                     println "TEST_INDY=\${testCompileTask.get().groovyOptions.optimizationOptions.indy}"
                     println "MAIN_JOINT_JAVAC_ARGS=\${compileTask.get().options.compilerArgs}"
                     println "MAIN_GROOVY_PARAMETERS=\${compileTask.get().groovyOptions.parameters}"
+                    println "MAIN_GROOVY_FORK_JVM_ARGS=\${compileTask.get().groovyOptions.forkOptions.jvmArgs}"
+                    println "TEST_GROOVY_FORK_JVM_ARGS=\${testCompileTask.get().groovyOptions.forkOptions.jvmArgs}"
                 }
             }
         """
@@ -107,6 +109,15 @@ class CompilePluginSpec extends Specification {
         result.output.contains('MAIN_JOINT_JAVAC_ARGS=[-parameters]')
     }
 
+    def "switches off the Spock Groovy version check in the forked Groovy compiler"() {
+        when:
+        def result = runPrintIndy()
+
+        then:
+        result.output.contains('MAIN_GROOVY_FORK_JVM_ARGS=[-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true]')
+        result.output.contains('TEST_GROOVY_FORK_JVM_ARGS=[-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true]')
+    }
+
     def "enables invokedynamic when grailsIndy is true"() {
         when:
         def result = runPrintIndy('-PgrailsIndy=true')
diff --git a/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/SbomPluginSpec.groovy b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/SbomPluginSpec.groovy
index 4786a254a9..3d4fe29f9f 100644
--- a/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/SbomPluginSpec.groovy
+++ b/build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/SbomPluginSpec.groovy
@@ -81,6 +81,34 @@ class SbomPluginSpec extends Specification {
         location.get().asFile.name == 'grails-core-custom-cli-8.0.0-sbom.json'
     }
 
+    private static List bsd4Choice() {
+        [[license: [id: 'BSD-4-Clause']]]
+    }
+
+    void "JLine 4 Maven jars use the BSD-3-Clause correction for any version of that line"() {
+        expect:
+        SbomPlugin.pickLicense(LOGGER, 'grails-console', 'grails-console',
+                'pkg:maven/org.jline/jansi@4.4.5?type=jar', bsd4Choice()).id == 'BSD-3-Clause'
+        SbomPlugin.pickLicense(LOGGER, 'grails-console', 'grails-console',
+                'pkg:maven/org.jline/jline-console-ui@4.5.0?type=jar', bsd4Choice()).id == 'BSD-3-Clause'
+    }
+
+    void "the JLine 4 license correction is limited to JLine 4 Maven jars"() {
+        when:
+        SbomPlugin.pickLicense(LOGGER, 'grails-console', 'grails-console', bomRef, bsd4Choice())
+
+        then:
+        GradleException e = thrown(GradleException)
+        e.message.contains('BSD-4-Clause')
+
+        where:
+        bomRef << [
+                'pkg:maven/com.example/jansi@4.4.5?type=jar',
+                'pkg:maven/org.jline/jansi@3.30.16?type=jar',
+                'pkg:maven/org.jline/jansi@4.4.5?type=pom',
+        ]
+    }
+
     void "a category-X license is permitted when exempted for the sbom component (cli companion)"() {
         expect: "the hibernate 5 LGPL exemption keyed to the dbmigration cli companion is honoured"
         SbomPlugin.pickLicense(LOGGER, 'grails-data-hibernate5-dbmigration',
diff --git a/build.gradle b/build.gradle
index 61f25361f5..247df0213d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -31,6 +31,8 @@ import javax.inject.Inject
 
 import org.apache.tools.ant.taskdefs.condition.Os
 
+apply from: layout.projectDirectory.file('dependencies.gradle')
+
 ext {
     isReproducibleBuild = System.getenv("SOURCE_DATE_EPOCH") != null
     buildInstant = java.util.Optional.ofNullable(System.getenv("SOURCE_DATE_EPOCH"))
@@ -139,6 +141,21 @@ subprojects {
         rootProject.ext['profileProjects'] << name
     }
 
+    // Groovy 6 (#16157, GROOVY-12185): the classic (non-invokedynamic) call-site runtime is the
+    // separate groovy-callsite module, and the compiler refuses to generate that bytecode without it
+    // on the compile classpath ("Classic call-site bytecode generation requires the optional
+    // org.apache.groovy:groovy-callsite module"). Every module compiles with indy off (CompilePlugin),
+    // and the ones below grails-common, which exports the module for everything above it, have no
+    // other way to get it. Permanent while indy stays off. Framework only: applications get it
+    // through grails-common.
+    plugins.withId('groovy') {
+        String groovyCallsite = "org.apache.groovy:groovy-callsite:${bomDependencyVersions['groovy.version']}"
+        sourceSets.configureEach { sourceSet ->
+            dependencies.add(sourceSet.compileOnlyConfigurationName, groovyCallsite)
+            dependencies.add(sourceSet.runtimeOnlyConfigurationName, groovyCallsite)
+        }
+    }
+
     configurations.configureEach {
         resolutionStrategy {
             def cacheHours = isCiBuild || isReproducibleBuild ? 0 : 24
diff --git a/dependencies.gradle b/dependencies.gradle
index 1e8e823280..6ebfeeec15 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -35,7 +35,7 @@ ext {
             'javaparser-core.version'       : '3.28.2',
             // Tracks the JLine version Groovy ships (groovy-groovysh), so the framework and groovysh
             // share one terminal stack - keep in step with groovy.version.
-            'jline.version'                 : '3.30.16',
+            'jline.version'                 : '4.4.5',
             'jna.version'                   : '5.19.1',
             'jquery.version'                : '4.0.0',
             'maven-model.version'           : '3.9.16',
@@ -85,7 +85,7 @@ ext {
             // extended-scalars compatibility. See https://github.com/apache/grails-core/issues/15674
             'graphql-java.version'          : '25.0',
             'graphql-java-extended-scalars.version': '24.0',
-            'groovy.version'                : '5.1.3',
+            'groovy.version'                : '6.0.0',
             'guava.version'                 : '33.7.1-jre',
             // Groovy YAML 5.1.0 requires a minimum of Jackson 2.22.1, and 2.22.2 also overrides the
             // 2.21.5 Spring Boot 4.1 manages: it adds the StreamReadConstraints length limit to
@@ -124,6 +124,11 @@ ext {
             'rxjava2.version'               : '2.2.21',
             'sitemesh.version'              : '2.6.0',
             'scribejava.version'            : '8.3.3',
+            // Groovy 6 (#16157): Spock has no groovy-6.0 build. The groovy-5.0 build runs on Groovy 6
+            // once its Groovy major-version check is switched off with the
+            // spock.iKnowWhatImDoing.disableGroovyVersionCheck system property, which CompilePlugin sets
+            // on every compile and test JVM here and the Grails Gradle plugin sets for applications.
+            // Move to the Groovy 6 build and drop the property when Spock publishes one. App test build.
             'spock.version'                 : '2.4-groovy-5.0',
             'starter-sitemesh.version'      : '3.3.0-RC1',
             'spring-webmvc-sitemesh.version': '3.3.0-RC1',
@@ -160,6 +165,9 @@ ext {
             'groovy'                   : "org.apache.groovy:groovy:${bomDependencyVersions['groovy.version']}",
             'groovy-ant'               : "org.apache.groovy:groovy-ant:${bomDependencyVersions['groovy.version']}",
             'groovy-astbuilder'        : "org.apache.groovy:groovy-astbuilder:${bomDependencyVersions['groovy.version']}",
+            // Groovy 6 (#16157, GROOVY-12185): the classic (non-invokedynamic) call-site runtime is a
+            // separate module. Grails compiles with indy off, so grails-common exports it. Permanent.
+            'groovy-callsite'          : "org.apache.groovy:groovy-callsite:${bomDependencyVersions['groovy.version']}",
             'groovy-cli-commons'       : "org.apache.groovy:groovy-cli-commons:${bomDependencyVersions['groovy.version']}",
             'groovy-cli-picocli'       : "org.apache.groovy:groovy-cli-picocli:${bomDependencyVersions['groovy.version']}",
             'groovy-console'           : "org.apache.groovy:groovy-console:${bomDependencyVersions['groovy.version']}",
diff --git a/end-to-end/README.md b/end-to-end/README.md
index 7d2dc6cfb6..35e55797fe 100644
--- a/end-to-end/README.md
+++ b/end-to-end/README.md
@@ -33,13 +33,13 @@ applications at via `GRAILS_REPO_URL`.
 | Project | What it is |
 |---|---|
 | `legacy-g7-command-plugin` | A **standalone build**, not part of this one. Compiles against published Grails 7 / Groovy 4 to produce a genuine precompiled `grails.dev.commands.ApplicationCommand` binary. |
-| `legacy-commands-plugin` | A Grails 8 plugin whose legacy commands are recompiled under Groovy 5. |
+| `legacy-commands-plugin` | A Grails 8 plugin whose legacy commands are recompiled under Groovy 6. |
 | `legacy-commands` | A Grails 8 application that consumes both and runs their commands through the registry. |
 | `spring-dependency-management` | A Grails 8 application that manages its versions with the legacy `io.spring.dependency-management` plugin instead of the Grails Gradle plugin's native `platform(grails-bom)`, as an upgraded Grails 7 application does. |
 | `taglib-index-incremental` | Builds a Grails 8 application **twice, without a clean**, to prove a renamed or deleted tag library cannot survive in the published tag library index. Incremental behaviour is the whole point, so it cannot be expressed by a project the core build builds once for itself. |
 
 `legacy-g7-command-plugin` is deliberately excluded from `settings.gradle`. An included build would
-substitute `org.apache.grails:grails-core` for this repository's Groovy 5 project, which is exactly
+substitute `org.apache.grails:grails-core` for this repository's Groovy 6 project, which is exactly
 the substitution the fixture exists to avoid — it must be compiled by a real Grails 7 toolchain for
 its trait-woven bytecode to prove anything.
 
diff --git a/end-to-end/build.gradle b/end-to-end/build.gradle
index 236237fd03..b93c75eb8d 100644
--- a/end-to-end/build.gradle
+++ b/end-to-end/build.gradle
@@ -29,6 +29,17 @@ subprojects {
         }
     }
 
+    // Grails 8 compiles against Groovy 6 while Spock ships as 2.4-groovy-5.0 (#16157). The
+    // framework build disables Spock's Groovy version check the same way (see CompilePlugin).
+    // Remove when the BOM moves to a Spock build for Groovy 6.
+    tasks.withType(GroovyCompile).configureEach {
+        groovyOptions.forkOptions.jvmArgs = (groovyOptions.forkOptions.jvmArgs ?: []) +
+                ['-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true']
+    }
+    tasks.withType(Test).configureEach {
+        systemProperty 'spock.iKnowWhatImDoing.disableGroovyVersionCheck', 'true'
+    }
+
     tasks.withType(AbstractCompile).configureEach {
         doFirst {
             if (!localMavenRepo.asFile.directory) {
diff --git a/end-to-end/legacy-g7-command-plugin/build.gradle b/end-to-end/legacy-g7-command-plugin/build.gradle
index 43981d6547..4168c282a8 100644
--- a/end-to-end/legacy-g7-command-plugin/build.gradle
+++ b/end-to-end/legacy-g7-command-plugin/build.gradle
@@ -22,7 +22,7 @@
 // sources under Grails 8.
 //
 // This is deliberately not part of the end-to-end build and is not composed with includeBuild:
-// substitution would rewrite org.apache.grails:grails-core to this repo's Groovy 5 project, which
+// substitution would rewrite org.apache.grails:grails-core to this repo's Groovy 6 project, which
 // is exactly what the fixture exists to avoid.
 //
 // It builds under the JDK declared in .sdkmanrc (17, the minimum for a Grails 7 app) rather than a
diff --git a/end-to-end/settings.gradle b/end-to-end/settings.gradle
index 428275ad5b..6880097cf7 100644
--- a/end-to-end/settings.gradle
+++ b/end-to-end/settings.gradle
@@ -121,7 +121,7 @@ include(
 
 // legacy-g7-command-plugin is deliberately NOT included here. It compiles against published
 // Grails 7 / Groovy 4 artifacts, and an included build would substitute
-// org.apache.grails:grails-core for this repo's Groovy 5 project - which is exactly the
+// org.apache.grails:grails-core for this repo's Groovy 6 project - which is exactly the
 // substitution the fixture exists to avoid. It stays a standalone build, run under its own JDK
 // (legacy-g7-command-plugin/.sdkmanrc), and legacy-commands consumes the jar it produces.
 
diff --git a/gradle/mongodb-forked-test-config.gradle b/gradle/mongodb-forked-test-config.gradle
index b230f36282..2b13265274 100644
--- a/gradle/mongodb-forked-test-config.gradle
+++ b/gradle/mongodb-forked-test-config.gradle
@@ -25,7 +25,9 @@ dependencies {
 }
 
 tasks.named('compileTestGroovy', GroovyCompile) {
-    groovyOptions.forkOptions.jvmArgs = ['-Xmx768m']
+    // Append rather than assign: CompilePlugin already passes the Spock Groovy version-check
+    // opt-out to this fork (#16157).
+    groovyOptions.forkOptions.jvmArgs = (groovyOptions.forkOptions.jvmArgs ?: []) + ['-Xmx768m']
 }
 
 tasks.withType(Test).configureEach {
diff --git a/gradle/mongodb-test-config.gradle b/gradle/mongodb-test-config.gradle
index 8925cf25de..cc3e3cfe5c 100644
--- a/gradle/mongodb-test-config.gradle
+++ b/gradle/mongodb-test-config.gradle
@@ -25,7 +25,9 @@ dependencies {
 }
 
 tasks.named('compileTestGroovy', GroovyCompile) {
-    groovyOptions.forkOptions.jvmArgs = ['-Xmx768m']
+    // Append rather than assign: CompilePlugin already passes the Spock Groovy version-check
+    // opt-out to this fork (#16157).
+    groovyOptions.forkOptions.jvmArgs = (groovyOptions.forkOptions.jvmArgs ?: []) + ['-Xmx768m']
 }
 
 tasks.withType(Test).configureEach {
diff --git a/grails-common/build.gradle b/grails-common/build.gradle
index 2b8ee5f649..0b2b471853 100644
--- a/grails-common/build.gradle
+++ b/grails-common/build.gradle
@@ -42,6 +42,13 @@ dependencies {
 
     api 'org.apache.grails.gradle:grails-gradle-common'
     api 'org.apache.groovy:groovy'
+    // Groovy 6 (#16157, GROOVY-12185): the classic (non-invokedynamic) call-site runtime is the
+    // separate groovy-callsite module. Grails compiles with indy off by default; without the module
+    // the compiler refuses to generate such bytecode and the classes fail at runtime with
+    // NoClassDefFoundError for CallSiteArray. Exporting it here puts it on the compile and runtime
+    // classpath of every module and application that reaches grails-common, which is everything
+    // that depends on grails-core. Permanent while indy stays off. App runtime.
+    api 'org.apache.groovy:groovy-callsite'
     api 'org.slf4j:jcl-over-slf4j'
     api 'org.slf4j:slf4j-api'
     api 'org.springframework:spring-context', {
diff --git a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy
index b6bfbbeaa3..28c14c1a5b 100644
--- a/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy
+++ b/grails-core/src/main/groovy/grails/boot/GrailsBanner.groovy
@@ -511,7 +511,8 @@ class GrailsBanner implements Banner {
 
     /** What an application wrote under the given property, or nothing where it wrote none. */
     private static List<String> readVersionOptions(Environment env, String propertyName) {
-        env.getProperty(propertyName, List<String>, [] as List<String>)
+        // Groovy 6.0.0 (GROOVY-12319, #16157): parameterized types are not class literals. Permanent. App source.
+        env.getProperty(propertyName, List, [] as List<String>) as List<String>
     }
 
     /**
diff --git a/grails-core/src/main/groovy/grails/config/external/WriterFilteringMap.groovy b/grails-core/src/main/groovy/grails/config/external/WriterFilteringMap.groovy
index 9667f67759..5819ea5318 100644
--- a/grails-core/src/main/groovy/grails/config/external/WriterFilteringMap.groovy
+++ b/grails-core/src/main/groovy/grails/config/external/WriterFilteringMap.groovy
@@ -26,7 +26,10 @@ class WriteFilteringMap implements Map<String, Object> {
     String keyPrefix
     private Map<String, Object> proxied // source map
 
-    @Delegate
+    // Groovy 6 (#16157): @Delegate now generates the mutators over the tracking overrides below, so
+    // written values were no longer recorded (WriteFilteringMapSpec). Excluded so the overrides stay.
+    // Remove when @Delegate again leaves declared methods alone. App runtime.
+    @Delegate(excludes = ['put', 'putAll', 'remove'])
     private Map<String, Object> overlap  // written values, flattened -- shared
     private Map<String, Object> nestedDestinationMap // written keys at this level
 
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy
index 9ffa2b0bdb..6b2acf0445 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormInstanceApi.groovy
@@ -524,7 +524,9 @@ class HibernateGormInstanceApi<D> extends GormInstanceApi<D> {
         setObjectToReadOnly target
         if (entity) {
             for (Association association in entity.associations) {
-                if (association instanceof ToOne && !association instanceof Embedded) {
+                // Groovy 6.0.0 (#16157) rejects `!x instanceof T` ("Incompatible instanceof types"),
+                // which parsed as `(!x) instanceof T` and was always false. Permanent. App source.
+                if (association instanceof ToOne && !(association instanceof Embedded)) {
                     def bean = new BeanWrapperImpl(target)
                     def propertyValue = bean.getPropertyValue(association.name)
                     if (propertyValue != null) {
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy
index d0fd9b01ed..70573d66c6 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormInstanceApiSpec.groovy
@@ -369,6 +369,25 @@ class HibernateGormInstanceApiSpec extends HibernateGormDatastoreSpec {
         person.errors.fieldErrors.any { it.field == 'name' }
     }
 
+    @Rollback
+    def "a validation error marks the invalid instance's to-one association read-only"() {
+        given: 'a persisted author and a book that fails validation'
+        def author = new PersonInstanceApi(name: 'Author', age: 40).save(flush: true)
+        def book = new ConstrainedBook(title: '', author: author)
+
+        when:
+        def result = ConstrainedBook.withTransaction {
+            book.save(flush: true)
+        }
+
+        then:
+        result == null
+        book.hasErrors()
+
+        and: 'the author the session still manages cannot be flushed behind the failed save'
+        ConstrainedBook.withSession { it.isReadOnly(author) }
+    }
+
     @Rollback
     def "save with invalid entity and failOnError:true throws an exception"() {
         given:
diff --git a/grails-datamapping-core/src/test/groovy/org/grails/compiler/gorm/GormEntityTransformSpec.groovy b/grails-datamapping-core/src/test/groovy/org/grails/compiler/gorm/GormEntityTransformSpec.groovy
index 3aeac5e308..0f01f808ef 100644
--- a/grails-datamapping-core/src/test/groovy/org/grails/compiler/gorm/GormEntityTransformSpec.groovy
+++ b/grails-datamapping-core/src/test/groovy/org/grails/compiler/gorm/GormEntityTransformSpec.groovy
@@ -229,12 +229,30 @@ class GormEntityTransformSpec extends Specification{
 
         expect: 'all GormEntity methods are marked as Generated on implementation class'
         GormEntity.methods.each { Method traitMethod ->
-            assert Book.getMethod(traitMethod.name, traitMethod.parameterTypes).isAnnotationPresent(Generated)
+            assert findGeneratedMethod(Book, traitMethod).isAnnotationPresent(Generated)
         }
 
         and: 'all GormValidateable methods are marked as Generated on implementation class'
         GormValidateable.methods.each { Method traitMethod ->
-            assert Book.getMethod(traitMethod.name, traitMethod.parameterTypes).isAnnotationPresent(Generated)
+            assert findGeneratedMethod(Book, traitMethod).isAnnotationPresent(Generated)
+        }
+    }
+
+    // Groovy 6 (#16157) specializes a generic trait method's type-variable parameters to the
+    // implementing class, so Book.merge(Object) from GormEntity<D> is Book.merge(Book). Look the
+    // specialized signature up when the erased one is missing. Remove if Groovy restores the erased
+    // signature. Test only.
+    private static Method findGeneratedMethod(Class targetClass, Method traitMethod) {
+        try {
+            return targetClass.getMethod(traitMethod.name, traitMethod.parameterTypes)
+        } catch (NoSuchMethodException e) {
+            Class[] specializedParameterTypes = traitMethod.genericParameterTypes.withIndex().collect { type, index ->
+                type instanceof java.lang.reflect.TypeVariable ? targetClass : traitMethod.parameterTypes[index]
+            } as Class[]
+            if (specializedParameterTypes.toList() != traitMethod.parameterTypes.toList()) {
+                return targetClass.getMethod(traitMethod.name, specializedParameterTypes)
+            }
+            throw e
         }
     }
 
diff --git a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/dirty/checking/DirtyCheckingList.groovy b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/dirty/checking/DirtyCheckingList.groovy
index 5ece3e8e33..d3155c7d9c 100644
--- a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/dirty/checking/DirtyCheckingList.groovy
+++ b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/dirty/checking/DirtyCheckingList.groovy
@@ -32,7 +32,10 @@ import groovy.transform.CompileStatic
 @CompileStatic
 class DirtyCheckingList extends DirtyCheckingCollection implements List {
 
-    @Delegate List target
+    // Groovy 6 (#16157): @Delegate now generates reversed() over the tracking override below, so the
+    // reverse-ordered view came back untracked (DirtyCheckingCollectionSpec). Excluded so the override
+    // stays. Remove when @Delegate again leaves declared methods alone. App runtime.
+    @Delegate(excludes = 'reversed') List target
 
     DirtyCheckingList(List target, DirtyCheckable parent, String property) {
         this(target, parent, property, false)
diff --git a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/dirty/checking/DirtyCheckingSortedSet.groovy b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/dirty/checking/DirtyCheckingSortedSet.groovy
index 6109b0426c..e6c3a8fc77 100644
--- a/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/dirty/checking/DirtyCheckingSortedSet.groovy
+++ b/grails-datastore-core/src/main/groovy/org/grails/datastore/mapping/dirty/checking/DirtyCheckingSortedSet.groovy
@@ -30,7 +30,8 @@ import groovy.transform.CompileStatic
 @CompileStatic
 class DirtyCheckingSortedSet extends DirtyCheckingCollection implements SortedSet {
 
-    @Delegate SortedSet target
+    // See the note on DirtyCheckingList (#16157): reversed() must not be generated over the override.
+    @Delegate(excludes = 'reversed') SortedSet target
 
     DirtyCheckingSortedSet(SortedSet target, DirtyCheckable parent, String property) {
         this(target, parent, property, false)
diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
index 79530c4673..6221f4e81b 100644
--- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
+++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc
@@ -1395,7 +1395,8 @@ grails.gorm.default.constraints = {
 [[groovy5-behavior-changes]]
 ==== 28. Apache Groovy 5 Behavior Changes
 
-Grails 8 upgrades from Apache Groovy 4 to Apache Groovy 5.
+Grails 8 upgrades from Apache Groovy 4 to Apache Groovy 6, so the Groovy 5 changes below apply on the way.
+See <<groovy6,Apache Groovy 6>> for what the move to Groovy 6 adds.
 Most applications need no changes, but a few stricter behaviors can affect application code.
 
 ===== 28.1 Assigning a static field from a static closure requires the class name
@@ -4093,3 +4094,51 @@ them. Name the connection, as in `@Transactional(connection = 'secondary')`, whe
 has to cover its writes.
 
 Applications using a single datasource are unaffected.
+
+[[groovy6]]
+==== 73. Apache Groovy 6
+
+Grails 8 builds on Apache Groovy 6.0. The Groovy 5 changes in <<groovy5-behavior-changes,Apache Groovy 5 Behavior Changes>>
+still apply; https://groovy-lang.org/releasenotes/groovy-6.0.html[the Groovy 6 release notes] list what changed since.
+Groovy 6 needs JDK 17 or later, which Grails 8's JDK 21 baseline already satisfies. Three of its changes touch a
+Grails build:
+
+* **The classic call-site runtime is a separate module.** Groovy 6 moves the non-`invokedynamic` call-site classes out
+  of `groovy` into `org.apache.groovy:groovy-callsite`, and a class compiled with `invokedynamic` off fails with
+  `NoClassDefFoundError: org/codehaus/groovy/runtime/callsite/CallSiteArray` without it, and the Groovy 6 compiler
+  refuses to generate such bytecode unless the module is on the compile classpath. Grails compiles applications that
+  way by default, and `grails-common`, which every application has through `grails-core`, exports the module, so it is
+  on the compile and runtime classpath already. Nothing needs adding, and a plugin or library compiled that way against
+  Groovy 4 or 5 keeps working. Groovy code compiled with `invokedynamic` off outside a Grails module, such as a plain
+  Groovy library in the same build, declares `org.apache.groovy:groovy-callsite` itself.
+* **Spock has no Groovy 6 build yet.** The Grails BOM keeps Spock `2.4-groovy-5.0`, which runs on Groovy 6 once its
+  Groovy major-version check is switched off. The Grails Gradle plugin sets
+  `spock.iKnowWhatImDoing.disableGroovyVersionCheck=true` on every `Test` task it configures; a task that already
+  sets the property keeps its own value. A `Test` task outside a Grails module, for example in a plain Groovy
+  library of a multi-project build that uses the Grails BOM, sets it itself:
+
+[source,groovy]
+----
+tasks.withType(Test).configureEach {
+    systemProperty 'spock.iKnowWhatImDoing.disableGroovyVersionCheck', 'true'
+}
+----
+
+* **JLine 4.** Groovy 6 ships `groovysh` on JLine 4.4.x, and the Grails BOM manages `org.jline:jline` at that version
+  for the Grails CLI and the console. An application that uses the JLine 3 API in its own code should check the
+  https://github.com/jline/jline3/releases[JLine 4 release notes] before upgrading.
+
+Four stricter compiler behaviours in Groovy 6.0.0 came up while moving Grails itself; application code with the same
+shape needs the same one-line change:
+
+* A parameterized type is no longer a class literal: write `config.getProperty(name, List, [])` rather than
+  `config.getProperty(name, List<String>, [])`, and `Mock(Foo)` rather than `Mock(Foo<Bar>)`.
+* `!x instanceof T` is rejected. It always parsed as `(!x) instanceof T`, which is never true, so the intended
+  `!(x instanceof T)` may change behaviour where the old form silently did nothing.
+* Under `@CompileStatic` or `@GrailsCompileStatic`, `new Foo(map)` on a class without a map constructor fails with
+  `Target constructor for constructor call expression hasn't been set`. Add `@groovy.transform.MapConstructor` to the class.
+* A Java source that refers to a Groovy class implementing a trait with static methods, such as `Validateable` or
+  a GORM entity, cannot be joint-compiled: the generated stub declares those methods `static abstract` and javac
+  rejects it. Until the Groovy stub generator is fixed, implement the static methods on the Groovy class
+  explicitly (delegating to `Validateable$Trait$Helper`, for instance), or move the Java source out of the joint
+  compilation.
diff --git a/grails-fields/src/main/groovy/grails/plugin/formfields/BeanPropertyAccessorImpl.groovy b/grails-fields/src/main/groovy/grails/plugin/formfields/BeanPropertyAccessorImpl.groovy
index 6e344c0033..6829d83b46 100644
--- a/grails-fields/src/main/groovy/grails/plugin/formfields/BeanPropertyAccessorImpl.groovy
+++ b/grails-fields/src/main/groovy/grails/plugin/formfields/BeanPropertyAccessorImpl.groovy
@@ -20,6 +20,7 @@ package grails.plugin.formfields
 
 import groovy.transform.Canonical
 import groovy.transform.CompileStatic
+import groovy.transform.MapConstructor
 import groovy.transform.Memoized
 import groovy.transform.TupleConstructor
 
@@ -42,6 +43,11 @@ import org.grails.scaffolding.model.property.Constrained
 @CompileStatic
 @Canonical
 @TupleConstructor(includes = ['beanType', 'propertyName', 'propertyType'])
+// Groovy 6.0.0 (#16157): @CompileStatic no longer resolves new BeanPropertyAccessorImpl(map) to the
+// implicit map-style construction ("Target constructor for constructor call expression hasn't been
+// set"). An explicit map constructor keeps that call shape. Keep permanently, or remove when the
+// implicit form compiles again. App source: a statically compiled `new Foo(map)` needs the same.
+@MapConstructor
 class BeanPropertyAccessorImpl implements BeanPropertyAccessor {
 
     Object rootBean
diff --git a/grails-fields/src/test/groovy/grails/plugin/formfields/BeanPropertyAccessorImplSpec.groovy b/grails-fields/src/test/groovy/grails/plugin/formfields/BeanPropertyAccessorImplSpec.groovy
new file mode 100644
index 0000000000..256aa787d4
--- /dev/null
+++ b/grails-fields/src/test/groovy/grails/plugin/formfields/BeanPropertyAccessorImplSpec.groovy
@@ -0,0 +1,51 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package grails.plugin.formfields
+
+import grails.core.GrailsApplication
+import spock.lang.Specification
+
+class BeanPropertyAccessorImplSpec extends Specification {
+
+    void "a map of properties constructs the accessor, as the factory does"() {
+        given:
+        def bean = new Object()
+        def grailsApplication = Mock(GrailsApplication)
+        Map<String, Object> params = [
+                rootBean: bean, rootBeanType: Object, pathFromRoot: 'name',
+                beanType: Object, propertyName: 'name', propertyType: String,
+                value: 'value', grailsApplication: grailsApplication,
+        ]
+
+        when:
+        def accessor = new BeanPropertyAccessorImpl(params)
+
+        then:
+        accessor.rootBean.is(bean)
+        accessor.rootBeanType == Object
+        accessor.pathFromRoot == 'name'
+        accessor.beanType == Object
+        accessor.propertyName == 'name'
+        accessor.propertyType == String
+        accessor.value == 'value'
+        accessor.grailsApplication.is(grailsApplication)
+        accessor.domainProperty == null
+        accessor.constraints == null
+    }
+}
diff --git a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
index c4a2a90bed..188ef5c0e2 100644
--- a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
+++ b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/core/GrailsGradlePlugin.groovy
@@ -99,6 +99,7 @@ import javax.inject.Inject
 @CompileStatic
 class GrailsGradlePlugin implements Plugin<Project> {
 
+    protected static final String SPOCK_DISABLE_GROOVY_VERSION_CHECK = 'spock.iKnowWhatImDoing.disableGroovyVersionCheck'
     private static final String NATIVE_IMAGE_PLUGIN = 'org.graalvm.buildtools.native'
 
     /** The plugin that compiles the pages, which are half of what a native image has to be told about. */
@@ -279,6 +280,13 @@ class GrailsGradlePlugin implements Plugin<Project> {
 
             project.tasks.withType(GroovyCompile).configureEach { GroovyCompile c ->
                 c.groovyOptions.optimizationOptions.indy = indyEnabled
+                // Groovy 6 with Spock 2.4-groovy-5.0 (#16157): Spock's global AST transform checks
+                // the Groovy major version inside the forked compiler too. Same property the Test
+                // tasks get, for the same reason; remove with it. App test build.
+                List<String> forkJvmArgs = c.groovyOptions.forkOptions.jvmArgs ?: []
+                if (!forkJvmArgs.any { String arg -> arg.startsWith("-D${SPOCK_DISABLE_GROOVY_VERSION_CHECK}=") }) {
+                    c.groovyOptions.forkOptions.jvmArgs = forkJvmArgs + ["-D${SPOCK_DISABLE_GROOVY_VERSION_CHECK}=true".toString()]
+                }
 
                 if (preserveParameterNames != null) {
                     project.logger.info('Grails: Configuring Groovy compilation to preserve parameter names: {}', preserveParameterNames)
@@ -1052,6 +1060,15 @@ ${importStatements}
 
         String grailsEnvSystemProperty = System.getProperty(Environment.KEY)
         tasks.withType(Test).configureEach(systemPropertyConfigurer.curry(grailsEnvSystemProperty ?: Environment.TEST.getName()))
+        tasks.withType(Test).configureEach { Test task ->
+            // Groovy 6 with Spock 2.4-groovy-5.0 (#16157): Spock refuses to start on Groovy 6 with
+            // IncompatibleGroovyVersionException unless its major-version check is switched off. An
+            // application that sets the property itself keeps its own value. Remove when the Grails
+            // BOM moves to a Spock build for Groovy 6. App test build.
+            if (!task.systemProperties.containsKey(SPOCK_DISABLE_GROOVY_VERSION_CHECK)) {
+                task.systemProperty(SPOCK_DISABLE_GROOVY_VERSION_CHECK, 'true')
+            }
+        }
         tasks.withType(JavaExec).configureEach(systemPropertyConfigurer.curry(grailsEnvSystemProperty ?: Environment.DEVELOPMENT.getName()))
 
         configureToolchainForForkTasks(project)
diff --git a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/views/AbstractGroovyTemplateCompileTask.groovy b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/views/AbstractGroovyTemplateCompileTask.groovy
index ff69e8a401..b32707c5aa 100644
--- a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/views/AbstractGroovyTemplateCompileTask.groovy
+++ b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/views/AbstractGroovyTemplateCompileTask.groovy
@@ -134,6 +134,17 @@ abstract class AbstractGroovyTemplateCompileTask extends AbstractCompile {
                         if (jvmArgs) {
                             javaExecSpec.jvmArgs(jvmArgs)
                         }
+
+                        // Groovy 6 with Spock 2.4-groovy-5.0 (#16157): the view template classpath can
+                        // carry Spock's global AST transform, which aborts compilation with
+                        // IncompatibleGroovyVersionException when the Groovy major is newer than the
+                        // Spock artifact's. This fork gets the same opt-out the Grails Gradle plugin
+                        // gives GroovyCompile and Test tasks, unless the build already passes one
+                        // through the fork options. Remove when the BOM moves to a Spock build for
+                        // Groovy 6. App test build.
+                        if (!jvmArgs?.any { String arg -> arg.startsWith('-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=') }) {
+                            javaExecSpec.systemProperty('spock.iKnowWhatImDoing.disableGroovyVersionCheck', 'true')
+                        }
                         javaExecSpec.maxHeapSize = compileOptions.forkOptions.memoryMaximumSize
                         javaExecSpec.minHeapSize = compileOptions.forkOptions.memoryInitialSize
 
diff --git a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainSpec.groovy b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainSpec.groovy
index 7f60c02a1f..cb8cf70e63 100644
--- a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainSpec.groovy
+++ b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/core/GrailsGradlePluginToolchainSpec.groovy
@@ -120,6 +120,15 @@ class GrailsGradlePluginToolchainSpec extends GradleSpecification {
         result.output.contains('HAS_ENV=true')
         result.output.contains('MIN_HEAP=768m')
         result.output.contains('MAX_HEAP=768m')
+
+        and: 'only Test tasks switch off the Spock Groovy major-version check, and a task that sets it keeps its value'
+        result.output.contains('JAVAEXEC_SPOCK_CHECK=null')
+        result.output.contains('TEST_HAS_ENV=true')
+        result.output.contains('TEST_SPOCK_CHECK=true')
+        result.output.contains('TEST_SPOCK_CHECK_OVERRIDDEN=false')
+
+        and: 'the forked Groovy compiler gets the same override for the Spock AST transform'
+        result.output.contains('COMPILE_FORK_JVM_ARGS=[-Dspock.iKnowWhatImDoing.disableGroovyVersionCheck=true]')
     }
 
     def "BootRun tasks enable Spring Boot console colors"() {
diff --git a/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/build.gradle b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/build.gradle
index 9816aab19d..2852dadfb2 100644
--- a/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/build.gradle
+++ b/grails-gradle/plugins/src/test/resources/test-projects/fork-settings-defaults/build.gradle
@@ -7,6 +7,17 @@ tasks.register('checkSysProps', JavaExec) {
     mainClass = 'does.not.Matter'
 }
 
+tasks.register('checkTestSysProps', Test) {
+    classpath = files()
+    testClassesDirs = files()
+}
+
+tasks.register('checkTestSysPropsOverridden', Test) {
+    classpath = files()
+    testClassesDirs = files()
+    systemProperty 'spock.iKnowWhatImDoing.disableGroovyVersionCheck', 'false'
+}
+
 tasks.register('inspectSysProps') {
     doLast {
         def task = tasks.named('checkSysProps', JavaExec).get()
@@ -14,5 +25,13 @@ tasks.register('inspectSysProps') {
         println "HAS_ENV=${sysProps.containsKey('grails.env')}"
         println "MIN_HEAP=${task.minHeapSize}"
         println "MAX_HEAP=${task.maxHeapSize}"
+        println "JAVAEXEC_SPOCK_CHECK=${sysProps.get('spock.iKnowWhatImDoing.disableGroovyVersionCheck')}"
+        def testTask = tasks.named('checkTestSysProps', Test).get()
+        println "TEST_HAS_ENV=${testTask.systemProperties.containsKey('grails.env')}"
+        println "TEST_SPOCK_CHECK=${testTask.systemProperties.get('spock.iKnowWhatImDoing.disableGroovyVersionCheck')}"
+        def overridden = tasks.named('checkTestSysPropsOverridden', Test).get()
+        println "TEST_SPOCK_CHECK_OVERRIDDEN=${overridden.systemProperties.get('spock.iKnowWhatImDoing.disableGroovyVersionCheck')}"
+        def compile = tasks.named('compileGroovy', GroovyCompile).get()
+        println "COMPILE_FORK_JVM_ARGS=${compile.groovyOptions.forkOptions.jvmArgs}"
     }
 }
diff --git a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3RenderViewMutatorSpec.groovy b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3RenderViewMutatorSpec.groovy
index e8751917d5..a6d8ea5591 100644
--- a/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3RenderViewMutatorSpec.groovy
+++ b/grails-gsp/grails-sitemesh3/src/test/groovy/org/grails/plugins/sitemesh3/Sitemesh3RenderViewMutatorSpec.groovy
@@ -38,7 +38,7 @@ class Sitemesh3RenderViewMutatorSpec extends Specification {
 
     GrailsSiteMeshView siteMeshView() {
         new GrailsSiteMeshView(innerView, Mock(ContentProcessor),
-                Mock(DecoratorSelector<SiteMeshContext>), Mock(ServletContext), Mock(ViewResolver))
+                Mock(DecoratorSelector) /* Groovy 6.0.0 (GROOVY-12319, #16157): parameterized types are not class literals */, Mock(ServletContext), Mock(ViewResolver))
     }
 
     void 'unwraps the SiteMesh view for partial renders without an explicit layout'() {
diff --git a/grails-spring-security/oauth2/plugin/grails-app/services/grails/plugin/springsecurity/oauth2/SpringSecurityOauth2BaseService.groovy b/grails-spring-security/oauth2/plugin/grails-app/services/grails/plugin/springsecurity/oauth2/SpringSecurityOauth2BaseService.groovy
index d72aa857e9..b097464035 100644
--- a/grails-spring-security/oauth2/plugin/grails-app/services/grails/plugin/springsecurity/oauth2/SpringSecurityOauth2BaseService.groovy
+++ b/grails-spring-security/oauth2/plugin/grails-app/services/grails/plugin/springsecurity/oauth2/SpringSecurityOauth2BaseService.groovy
@@ -301,7 +301,8 @@ class SpringSecurityOauth2BaseService {
      * @return The role names for a newly registered user
      */
     def getRoleNames() {
-        def roleNames = grailsApplication.config.getProperty('grails.plugin.springsecurity.oauth2.registration.roleNames', List<String>, ['ROLE_USER'])
+        // Groovy 6.0.0 (GROOVY-12319, #16157): parameterized types are not class literals. Permanent.
+        def roleNames = grailsApplication.config.getProperty('grails.plugin.springsecurity.oauth2.registration.roleNames', List, ['ROLE_USER'])
         return roleNames
     }
 }
diff --git a/grails-testing-support-core/src/main/groovy/org/grails/testing/ParameterizedGrailsUnitTest.groovy b/grails-testing-support-core/src/main/groovy/org/grails/testing/ParameterizedGrailsUnitTest.groovy
index 60e1e7e8ee..291b9a6e45 100755
--- a/grails-testing-support-core/src/main/groovy/org/grails/testing/ParameterizedGrailsUnitTest.groovy
+++ b/grails-testing-support-core/src/main/groovy/org/grails/testing/ParameterizedGrailsUnitTest.groovy
@@ -49,7 +49,8 @@ trait ParameterizedGrailsUnitTest<T> extends GrailsUnitTest {
                 mockArtefact(cutType)
                 final String beanName = getBeanName(cutType)
                 if (beanName != null && applicationContext.containsBean(beanName)) {
-                    _artefactInstance = applicationContext.getBean(beanName, T)
+                    // Groovy 6.0.0 (GROOVY-12319, #16157): a type parameter is not a class literal. Permanent.
+                    _artefactInstance = applicationContext.getBean(beanName, cutType)
                 } else {
                     _artefactInstance = cutType.newInstance()
                     applicationContext.autowireCapableBeanFactory.autowireBeanProperties(_artefactInstance, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false)
diff --git a/grails-testing-support-mongodb/src/main/groovy/org/apache/grails/testing/mongo/StartMongoGrailsUnitExtension.groovy b/grails-testing-support-mongodb/src/main/groovy/org/apache/grails/testing/mongo/StartMongoGrailsUnitExtension.groovy
index 0007914d76..46ef63c63e 100644
--- a/grails-testing-support-mongodb/src/main/groovy/org/apache/grails/testing/mongo/StartMongoGrailsUnitExtension.groovy
+++ b/grails-testing-support-mongodb/src/main/groovy/org/apache/grails/testing/mongo/StartMongoGrailsUnitExtension.groovy
@@ -100,7 +100,8 @@ class StartMongoGrailsUnitExtension extends AbstractMongoGrailsExtension impleme
                         Package[] packagesArray = packages.toArray(new Package[packages.size()])
 
                         Map<String, Object> configuration = ['grails.mongodb.url': createConnectionString(container.getHost(), container.getMappedPort(DEFAULT_MONGO_PORT))]
-                        def datastore = mongoDatastoreClass.getDeclaredConstructor(Map<String, Object>, Package[]).newInstance(configuration, packagesArray)
+                        // Groovy 6.0.0 (GROOVY-12319, #16157): parameterized types are not class literals. Permanent.
+                        def datastore = mongoDatastoreClass.getDeclaredConstructor(Map, Package[]).newInstance(configuration, packagesArray)
                         mongoDatastoreField.writeValue(invocation.sharedInstance, datastore)
                     }
                 }
diff --git a/grails-validation/src/test/groovy/grails/validation/ValidateableTraitSpec.groovy b/grails-validation/src/test/groovy/grails/validation/ValidateableTraitSpec.groovy
index 06b1cb9aa1..76cb3f26b4 100644
--- a/grails-validation/src/test/groovy/grails/validation/ValidateableTraitSpec.groovy
+++ b/grails-validation/src/test/groovy/grails/validation/ValidateableTraitSpec.groovy
@@ -347,6 +347,19 @@ class MyValidateable implements Validateable {
     String town
     private String _someProperty = 'default value'
 
+    // Groovy 6.0.0 joint compilation (#16157): SomeJavaClass.java references this class, so the
+    // Groovy compiler writes a Java stub for it, and its stub generator emits a precompiled trait's
+    // static methods as `static abstract`, which javac rejects. Implementing them here keeps the
+    // stub legal. Remove when the stub generator handles static trait methods. App source: a Java
+    // class referencing a Groovy class that implements Validateable hits the same failure.
+    static Map<String, Constrained> getConstraintsMap() {
+        Validateable$Trait$Helper.getConstraintsMap(MyValidateable)
+    }
+
+    static void clearConstraintsMapCache() {
+        Validateable$Trait$Helper.clearConstraintsMapCache(MyValidateable)
+    }
+
     void setSomeOtherProperty(String s) {}
 
     void setSomeProperty(String s) {
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/view/api/GrailsJsonViewHelper.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/view/api/GrailsJsonViewHelper.groovy
index 114317700e..9d3a47619a 100644
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/view/api/GrailsJsonViewHelper.groovy
+++ b/grails-views-gson/src/main/groovy/grails/plugin/json/view/api/GrailsJsonViewHelper.groovy
@@ -45,7 +45,15 @@ interface GrailsJsonViewHelper extends GrailsViewHelper {
      * @param arguments The named arguments: 'template', 'collection', 'model', 'var' and 'bean'
      * @return The unescaped JSON
      */
-    JsonOutput.JsonWritable render(Map arguments)
+    // Groovy 6.0.0 (#16157): the concrete render(...) overrides in DefaultGrailsJsonViewHelper get a
+    // different return-type descriptor from these methods when JsonOutput.JsonWritable, a nested
+    // class of the joint-compiled JsonOutput.java, is resolved, so the abstract-method check
+    // reports them unimplemented ("Can't have an abstract method in a non-abstract class").
+    // Declared default so the check has nothing to flag; every implementor overrides them. Remove
+    // when the compiler resolves both sides alike. Framework only.
+    default JsonOutput.JsonWritable render(Map arguments) {
+        throw new UnsupportedOperationException()
+    }
 
     /**
      * Renders the given object to JSON, typically a domain class, ignoring lazy and internal properties
@@ -55,7 +63,9 @@ interface GrailsJsonViewHelper extends GrailsViewHelper {
      * @param customizer Used to customize the contents
      * @return The unescaped JSON
      */
-    JsonOutput.JsonWritable render(Object object, Map arguments, @DelegatesTo(StreamingJsonBuilder.StreamingJsonDelegate) Closure customizer)
+    default JsonOutput.JsonWritable render(Object object, Map arguments, @DelegatesTo(StreamingJsonBuilder.StreamingJsonDelegate) Closure customizer) {
+        throw new UnsupportedOperationException()
+    }
 
     /**
      * Renders the given object to JSON, typically a domain class, ignoring lazy and internal properties
@@ -64,7 +74,9 @@ interface GrailsJsonViewHelper extends GrailsViewHelper {
      * @param arguments The supported named arguments: 'includes' or 'excludes' list
      * @return The unescaped JSON
      */
-    JsonOutput.JsonWritable render(Object object, Map arguments)
+    default JsonOutput.JsonWritable render(Object object, Map arguments) {
+        throw new UnsupportedOperationException()
+    }
 
     /**
      * Renders the given object to JSON, typically a domain class, ignoring lazy and internal properties
@@ -72,7 +84,9 @@ interface GrailsJsonViewHelper extends GrailsViewHelper {
      * @param object The object to render
      * @return The unescaped JSON
      */
-    JsonOutput.JsonWritable render(Object object)
+    default JsonOutput.JsonWritable render(Object object) {
+        throw new UnsupportedOperationException()
+    }
 
     /**
      * Renders the given object to JSON, typically a domain class, ignoring lazy and internal properties
@@ -81,7 +95,9 @@ interface GrailsJsonViewHelper extends GrailsViewHelper {
      * @param customizer the customizer
      * @return The unescaped JSON
      */
-    JsonOutput.JsonWritable render(Object object, @DelegatesTo(StreamingJsonBuilder.StreamingJsonDelegate) Closure customizer)
+    default JsonOutput.JsonWritable render(Object object, @DelegatesTo(StreamingJsonBuilder.StreamingJsonDelegate) Closure customizer) {
+        throw new UnsupportedOperationException()
+    }
 
     /**
      * Renders the given object inline within the current JSON object instead of creating a new JSON object
diff --git a/grails-views-gson/src/main/groovy/grails/plugin/json/view/api/internal/TemplateRenderer.groovy b/grails-views-gson/src/main/groovy/grails/plugin/json/view/api/internal/TemplateRenderer.groovy
index 905a7ad385..3073ce9cda 100644
--- a/grails-views-gson/src/main/groovy/grails/plugin/json/view/api/internal/TemplateRenderer.groovy
+++ b/grails-views-gson/src/main/groovy/grails/plugin/json/view/api/internal/TemplateRenderer.groovy
@@ -19,8 +19,10 @@
 
 package grails.plugin.json.view.api.internal
 
+import groovy.json.StreamingJsonBuilder
 import groovy.transform.CompileStatic
 
+import grails.plugin.json.builder.JsonOutput
 import grails.plugin.json.view.api.GrailsJsonViewHelper
 import grails.util.GrailsNameUtils
 
@@ -39,6 +41,39 @@ class TemplateRenderer {
         this.jsonViewHelper = jsonViewHelper
     }
 
+    // Groovy 6.0.0 (#16157): @Delegate no longer satisfies the abstract-method check for the five
+    // render(...) overloads, whose return type is a nested class of the joint-compiled
+    // JsonOutput.java ("Can't have an abstract method in a non-abstract class"). The void
+    // inline(...) overloads are unaffected, so @Delegate still handles them. Same behaviour as the
+    // forwarders @Delegate generated on Groovy 5. Remove when @Delegate handles the return type.
+    // Framework only.
+
+    @Override
+    JsonOutput.JsonWritable render(Map arguments) {
+        jsonViewHelper.render(arguments)
+    }
+
+    @Override
+    JsonOutput.JsonWritable render(Object object, Map arguments,
+                                   @DelegatesTo(StreamingJsonBuilder.StreamingJsonDelegate) Closure customizer) {
+        jsonViewHelper.render(object, arguments, customizer)
+    }
+
+    @Override
+    JsonOutput.JsonWritable render(Object object, Map arguments) {
+        jsonViewHelper.render(object, arguments)
+    }
+
+    @Override
+    JsonOutput.JsonWritable render(Object object) {
+        jsonViewHelper.render(object)
+    }
+
+    @Override
+    JsonOutput.JsonWritable render(Object object, @DelegatesTo(StreamingJsonBuilder.StreamingJsonDelegate) Closure customizer) {
+        jsonViewHelper.render(object, customizer)
+    }
+
     @Override
     Object invokeMethod(String name, Object args) {
         Object[] argArray = (Object[]) args
diff --git a/grails-views-gson/src/test/groovy/grails/plugin/json/view/TemplateRendererSpec.groovy b/grails-views-gson/src/test/groovy/grails/plugin/json/view/TemplateRendererSpec.groovy
index ddcffecfaf..1cb608d05f 100644
--- a/grails-views-gson/src/test/groovy/grails/plugin/json/view/TemplateRendererSpec.groovy
+++ b/grails-views-gson/src/test/groovy/grails/plugin/json/view/TemplateRendererSpec.groovy
@@ -27,6 +27,29 @@ import spock.lang.Specification
  */
 class TemplateRendererSpec extends Specification {
 
+    void "the render overloads of the view helper are available on the template renderer and forward to it"() {
+        given: "a template renderer"
+        def mockViewHelper = Mock(GrailsJsonViewHelper)
+        def tmpl = new TemplateRenderer(mockViewHelper)
+        def o = new Object()
+        def customizer = { -> }
+
+        when:
+        tmpl.render(template: 'foo')
+        tmpl.render(o)
+        tmpl.render(o, [includes: ['name']])
+        tmpl.render(o, customizer)
+        tmpl.render(o, [excludes: ['name']], customizer)
+
+        then:
+        1 * mockViewHelper.render([template: 'foo'])
+        1 * mockViewHelper.render(o)
+        1 * mockViewHelper.render(o, [includes: ['name']])
+        1 * mockViewHelper.render(o, customizer)
+        1 * mockViewHelper.render(o, [excludes: ['name']], customizer)
+        0 * _
+    }
+
     void "Test template renderer calls the correct render method"() {
         given:"A template renderer"