Summary
Any Grails 7 plugin that ships a Groovy @Immutable class compiled with Groovy 4 fails at runtime on Grails 8 with NoClassDefFoundError: org/apache/groovy/runtime/ObjectUtil. Groovy 4's @Immutable AST transformation emits calls to org.apache.groovy.runtime.ObjectUtil.cloneObject(Object) for defensive copying, and that class was removed in Groovy 5.
The plugin resolves, compiles, loads, and boots. The failure only appears when the affected class is actually constructed, so a smoke test that merely starts the application will pass.
Found while auditing released Grails 7 plugins against 8.0.0-M5, using io.github.matrei:grails-inertia:3.0.0 (latest release, catalog constraint 7.0.0 > *).
Grails Version
8.0.0-M5 (Groovy 5.0.8, Spring Boot 4.1.0, Spring Framework 7.0.8, JDK 21.0.11)
Steps to Reproduce
In a stock create-app --profile=web application on 8.0.0-M5:
implementation "io.github.matrei:grails-inertia:3.0.0"
Complete the plugin's documented client setup (npm install && npm run build, which produces the Vite manifest the plugin requires outside development), add a controller action that calls the plugin's documented renderInertia(...), then issue a real request:
GET /inertia-probe with header X-Inertia: true
Actual Behaviour
The application starts normally - Tomcat started on port 54517, and the plugin appears in the load order as inertia (3.0.0). The request returns HTTP 500:
jakarta.servlet.ServletException: Handler dispatch failed:
java.lang.NoClassDefFoundError: org/apache/groovy/runtime/ObjectUtil
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:978)
...
at grails.plugin.inertia.InertiaPage.<init>(InertiaPage.groovy)
at grails.plugin.inertia.Inertia.createJsonModel(Inertia.groovy:148)
at grails.plugin.inertia.InertiaTrait$Trait$Helper.renderInertia(...)
Root Cause
org.apache.groovy.runtime.ObjectUtil exists in Groovy 4 and is gone in Groovy 5. Verified directly against the jars on the resolved classpath:
groovy-4.0.32.jar : org/apache/groovy/runtime/ObjectUtil.class present = True
groovy-5.0.8.jar : org/apache/groovy/runtime/ObjectUtil.class present = False
groovy-3.0.25.jar : org/apache/groovy/runtime/ObjectUtil.class present = False
The plugin's InertiaPage is a Groovy immutable class - its constant pool carries groovy.transform.KnownImmutable and a call to org.codehaus.groovy.transform.ImmutableASTTransformation.checkPropNames:
#13 = Utf8 Lgroovy/transform/KnownImmutable;
#145 = Utf8 org/codehaus/groovy/transform/ImmutableASTTransformation
41: invokestatic #150 // ImmutableASTTransformation.checkPropNames:(Ljava/lang/Object;Ljava/util/Map;)V
and the Groovy 4 @Immutable transformation compiled 22 defensive-copy call sites into that single class:
77: invokestatic #68 // Method org/apache/groovy/runtime/ObjectUtil.cloneObject:(Ljava/lang/Object;)Ljava/lang/Object;
87: invokestatic #68 // Method org/apache/groovy/runtime/ObjectUtil.cloneObject:(Ljava/lang/Object;)Ljava/lang/Object;
102: invokestatic #68 // Method org/apache/groovy/runtime/ObjectUtil.cloneObject:(Ljava/lang/Object;)Ljava/lang/Object;
... (22 total)
Every one of those invokestatic targets is missing at runtime under Groovy 5, so constructing the class throws.
Expected Behaviour
At minimum this belongs in the Grails 8 upgrade guide's Groovy 5 section: a Groovy 4-compiled @Immutable class cannot run on Groovy 5 and must be recompiled. Because the emitted call sites are generated by the AST transformation rather than written by the author, nothing in the plugin's source hints at the dependency, and there is no build-time signal.
Why this is worth tracking here
The trigger is a plain, idiomatic Groovy annotation, not an exotic API:
@Immutable is ordinary Groovy used widely for value/DTO types.
- The failure survives dependency resolution, compilation, plugin load, and application startup.
- The error names a Groovy internal class and gives no indication that an
@Immutable class or a stale plugin is responsible.
- It only surfaces when that class is instantiated, so it can hide behind untested code paths.
This is the third distinct Groovy 4 -> 5 binary break found in this sweep, after #16123 (generic trait with fields) and #16126 (AST transformation targeting a private trait method). Together they suggest the upgrade guide would benefit from a consolidated "Groovy 4-compiled plugins can fail at runtime" section, since all three pass every build-time check.
Distinct from previously reported issues
Not #16122 (Metadata dynamic key access), #16123 ($Trait$FieldHelper / MalformedParameterizedTypeException), #16124 (partial Jackson 2 classpath), #16125 (pre-Apache org.grails coordinates), #16126 (private trait method lost by an AST transformation), or #16127 (Spring 7 HttpHeaders no longer implementing MultiValueMap). This one is a removed Groovy runtime class referenced by @Immutable-generated code.
Notes
Part of a compatibility sweep of released Grails 7 plugins against 8.0.0-M5. In the same sweep cloud.wondrify:i18n-asset-pipeline-grails:5.2.0-M1, cloud.wondrify:karman-grails:3.0.6, and io.github.gpc:phone-number-constraint:2.0.0 passed verification against their documented public APIs.
Summary
Any Grails 7 plugin that ships a Groovy
@Immutableclass compiled with Groovy 4 fails at runtime on Grails 8 withNoClassDefFoundError: org/apache/groovy/runtime/ObjectUtil. Groovy 4's@ImmutableAST transformation emits calls toorg.apache.groovy.runtime.ObjectUtil.cloneObject(Object)for defensive copying, and that class was removed in Groovy 5.The plugin resolves, compiles, loads, and boots. The failure only appears when the affected class is actually constructed, so a smoke test that merely starts the application will pass.
Found while auditing released Grails 7 plugins against
8.0.0-M5, usingio.github.matrei:grails-inertia:3.0.0(latest release, catalog constraint7.0.0 > *).Grails Version
8.0.0-M5 (Groovy 5.0.8, Spring Boot 4.1.0, Spring Framework 7.0.8, JDK 21.0.11)
Steps to Reproduce
In a stock
create-app --profile=webapplication on 8.0.0-M5:implementation "io.github.matrei:grails-inertia:3.0.0"Complete the plugin's documented client setup (
npm install && npm run build, which produces the Vite manifest the plugin requires outside development), add a controller action that calls the plugin's documentedrenderInertia(...), then issue a real request:Actual Behaviour
The application starts normally -
Tomcat started on port 54517, and the plugin appears in the load order asinertia (3.0.0). The request returns HTTP 500:Root Cause
org.apache.groovy.runtime.ObjectUtilexists in Groovy 4 and is gone in Groovy 5. Verified directly against the jars on the resolved classpath:The plugin's
InertiaPageis a Groovy immutable class - its constant pool carriesgroovy.transform.KnownImmutableand a call toorg.codehaus.groovy.transform.ImmutableASTTransformation.checkPropNames:and the Groovy 4
@Immutabletransformation compiled 22 defensive-copy call sites into that single class:Every one of those
invokestatictargets is missing at runtime under Groovy 5, so constructing the class throws.Expected Behaviour
At minimum this belongs in the Grails 8 upgrade guide's Groovy 5 section: a Groovy 4-compiled
@Immutableclass cannot run on Groovy 5 and must be recompiled. Because the emitted call sites are generated by the AST transformation rather than written by the author, nothing in the plugin's source hints at the dependency, and there is no build-time signal.Why this is worth tracking here
The trigger is a plain, idiomatic Groovy annotation, not an exotic API:
@Immutableis ordinary Groovy used widely for value/DTO types.@Immutableclass or a stale plugin is responsible.This is the third distinct Groovy 4 -> 5 binary break found in this sweep, after #16123 (generic trait with fields) and #16126 (AST transformation targeting a private trait method). Together they suggest the upgrade guide would benefit from a consolidated "Groovy 4-compiled plugins can fail at runtime" section, since all three pass every build-time check.
Distinct from previously reported issues
Not #16122 (Metadata dynamic key access), #16123 (
$Trait$FieldHelper/MalformedParameterizedTypeException), #16124 (partial Jackson 2 classpath), #16125 (pre-Apacheorg.grailscoordinates), #16126 (private trait method lost by an AST transformation), or #16127 (Spring 7HttpHeadersno longer implementingMultiValueMap). This one is a removed Groovy runtime class referenced by@Immutable-generated code.Notes
Part of a compatibility sweep of released Grails 7 plugins against 8.0.0-M5. In the same sweep
cloud.wondrify:i18n-asset-pipeline-grails:5.2.0-M1,cloud.wondrify:karman-grails:3.0.6, andio.github.gpc:phone-number-constraint:2.0.0passed verification against their documented public APIs.