Grails 7 plugins exposing a generic trait with fields fail with MalformedParameterizedTypeException on Grails 8
- Dominant language
- Groovy
- Stars
- 2.9k
- Forks
- 975
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 92
Description
### Summary
A Grails 7 plugin that ships a **generic trait containing fields** cannot be used by a Grails 8 application. The Groovy 4 compiler emitted a **non-generic** `$Trait$FieldHelper` interface for such traits, while the Groovy 5 compiler used by Grails 8 emits a **generic** one and, when compiling an implementing class, writes a parameterized reference to it.
The result is a class whose `Signature` attribute references `FieldHelper` while the actual interface on the classpath declares zero type parameters. The class loads, but any call to `Class.getGenericInterfaces()` throws:
```
java.lang.reflect.MalformedParameterizedTypeException: Mismatch of count of formal and actual type arguments in constructor of $Trait$FieldHelper: 0 formal argument(s) 1 actual argument(s)
```
`java.beans.Introspector`, Spring, and Jackson all call `getGenericInterfaces()`, so this fires on ordinary application paths.
Discovered while testing released Grails 7 plugins against `8.0.0-M5`.
### Grails Version
8.0.0-M5
### Java / Groovy Version
Java 21.0.11 (Corretto), Groovy 5.0.8, Spring Boot 4.1.0, Spring Framework 7.0.8
### Real plugin affected
`org.grails.plugins:grails-logical-delete:3.0.0` - the Grails 7 release announced in #15044. Its public API is the generic trait `grails.logical.delete.LogicalDelete`, which has fields.
### Steps to Reproduce
1. Generate a stock web app with the published 8.0.0-M5 distribution.
2. Add `implementation "org.grails.plugins:grails-logical-delete:3.0.0"`.
3. Add a domain class implementing the plugin trait:
```groovy
package logical.delete
import grails.logical.delete.LogicalDelete
class LogicalDeletedRecord implements LogicalDelete {
String name
static constraints = { name nullable: false }
}
```
4. Exercise it through the ordinary public GORM API:
```groovy
@Integration
@Rollback
class LogicalDeleteCompatibilitySpec extends Specification {
void 'delete hides a logical-delete domain instance from public GORM lookups'() {
given:
LogicalDeletedRecord record = new LogicalDeletedRecord(name: 'release-3.0.0').save(failOnError: true, flush: true)
...
}
}
```
### Actual Behaviour
The plugin resolves and loads - `logicalDelete (3.0.0)` appears in the plugin list - and the context starts. The test fails on the very first line, the map constructor:
```
java.lang.reflect.MalformedParameterizedTypeException: Mismatch of count of formal and actual type arguments in constructor of grails.logical.delete.LogicalDelete$Trait$FieldHelper: 0 formal argument(s) 1 actual argument(s)
at java.base/java.lang.Class.getGenericInterfaces(Class.java:1298)
at java.desktop/com.sun.beans.TypeResolver.prepare(TypeResolver.java:312)
at java.desktop/java.beans.FeatureDescriptor.getParameterTypes(FeatureDescriptor.java:391)
at java.desktop/java.beans.MethodDescriptor.setMethod(MethodDescriptor.java:118)
at java.desktop/java.beans.Introspector.getTargetMethodInfo(Introspector.java:1030)
at java.desktop/java.beans.Introspector.getBeanInfo(Introspector.java:446)
at java.desktop/java.beans.Introspector.getBeanInfo(Introspector.java:195)
at logical.delete.LogicalDeleteCompatibilitySpec.$tt__$spock_feature_0_0(LogicalDeleteCompatibilitySpec.groovy:28)
```
### Root cause evidence
`javap` on the plugin jar (compiled by Groovy 4 for Grails 7) - the trait is generic but its `FieldHelper` is **not**:
```
public interface grails.logical.delete.LogicalDelete extends org.grails.datastore.gorm.GormEntity
Signature: Ljava/lang/Object;Lorg/grails/datastore/gorm/GormEntity;
public interface grails.logical.delete.LogicalDelete$Trait$FieldHelper
```
`javap` on the domain class compiled by Grails 8 - it references that non-generic interface **with a type argument**:
```
public class logical.delete.LogicalDeletedRecord extends java.lang.Object
implements grails.logical.delete.LogicalDelete,
grails.logical.delete.LogicalDelete$Trait$FieldHelper,
org.grails.datastore.mapping.dirty.checking.DirtyCheckable$Trait$FieldHelper,
org.grails.datastore.gorm.GormValidateable$Trait$FieldHelper,
groovy.lang.GroovyObject
```
Note the contrast in the same signature: the Grails 8 (Groovy 5 compiled) `DirtyCheckable$Trait$FieldHelper` and `GormValidateable$Trait$FieldHelper` are non-generic traits and are referenced without type arguments, so they are fine.
Confirming that Groovy 5 is self-consistent, this probe compiled inside the same 8.0.0-M5 app produces a **generic** `FieldHelper`:
```groovy
trait GenericFieldTrait {
String probeField
D probeValue
}
class ProbeUser implements GenericFieldTrait {}
```
```
public interface probe.GenericFieldTrait$Trait$FieldHelper
Signature: Ljava/lang/Object;
public class probe.ProbeUser ... implements probe.GenericFieldTrait, probe.GenericFieldTrait$Trait$FieldHelper, groovy.lang.GroovyObject
```
So Groovy 5 consistently emits `FieldHelper` and references it as `FieldHelper`. Against a Groovy 4-compiled plugin whose `FieldHelper` has no type parameters, the emitted signature is invalid.
### Expected Behaviour
Either a Grails 7 plugin exposing a generic trait with fields remains usable on Grails 8, or the incompatibility is detected and reported clearly (and documented), rather than surfacing as a `MalformedParameterizedTypeException` from JavaBeans introspection.
### Impact
This affects any Grails 7 plugin whose public extension point is a generic trait carrying fields, which is a common plugin pattern for GORM-facing traits. The failure mode is poor: resolution, plugin discovery, and startup all succeed, and the error appears later from an unrelated-looking JDK introspection frame.
Plugins whose traits are non-generic are unaffected - verified in the same sweep with `grails-shiro:6.0.0`, whose `GrailsShiroRealm$Trait$FieldHelper` and `TypedNamedArgs$Trait$FieldHelper` are non-generic and which loads and wires correctly on 8.0.0-M5.
### Suggested resolution
Options for discussion:
1. Detect the mismatch when Grails compiles a class against a trait whose `FieldHelper` arity does not match, and emit a clear compile-time error naming the plugin and trait instead of producing an invalid signature.
2. Raise upstream with Groovy if the Groovy 4 to Groovy 5 `FieldHelper` generics change is considered a trait ABI break that should be tolerated when compiling against older trait bytecode.
3. At minimum, document this in the Grails 8 upgrade guide's plugin-compatibility section: a Grails 7 plugin exposing a generic trait with fields must be recompiled with Groovy 5 and re-released for Grails 8.
### Notes
Found in a broader sweep of released Grails 7 plugins against 8.0.0-M5. Related compatibility-shim precedents: #16011 (legacy command plugins) and #16101 (legacy `Holders` access during `doWithSpring`).
Contributor guide
Research direction
Start with the LogicalDeleteCompatibilitySpec reproducer and inspect the plugin and generated domain-class signatures with javap. Compare the Groovy 4 and Groovy 5 FieldHelper declarations and references; done means the incompatibility is either handled with a clear diagnostic or documented for Grails 8 plugin compatibility.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- groovy
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100