Grails 7 plugins break at runtime after removal of Metadata dynamic and map-style key access
- Dominant language
- Groovy
- Stars
- 2.9k
- Forks
- 975
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 92
Description
### Summary
Grails 8 removed `grails.util.Metadata.get(Object)` and `grails.util.Metadata.getProperty(String)`. Because both were the entry points Groovy uses for dynamic/map-style access, any Grails 7 plugin or application that reads a metadata key with `Metadata.current.'some.key'` or `Metadata.current.get('some.key')` now fails at **runtime** with `MissingPropertyException` / `MissingMethodException` instead of returning a value or `null`.
This is not caught at compile time, so an unchanged Grails 7 binary plugin installs, loads, and boots normally and then throws a 500 on the first request that touches the code path.
Discovered while testing released Grails 7 plugins against `8.0.0-M5`.
### Grails Version
8.0.0-M5 (also expected on current 8.0.x)
### Java / Groovy Version
Java 21.0.11 (Corretto), Groovy 5.0.8, Spring Boot 4.1.0, Spring Framework 7.0.8
### Steps to Reproduce
Minimal reproduction in a stock `grails create-app --profile=web` application generated with the published 8.0.0-M5 distribution:
```groovy
package probe
import grails.util.Metadata
import spock.lang.Specification
class MetadataAccessSpec extends Specification {
void 'dynamic key access on Metadata still works'() {
expect:
Metadata.current.'app.grails.version' == null
}
void 'map style get on Metadata still works'() {
expect:
Metadata.current.get('app.grails.version') == null
}
void 'supported typed accessor works'() {
expect:
Metadata.current.getProperty('app.grails.version', String, null) == null
}
}
```
Result on 8.0.0-M5:
```
MetadataAccessSpec > dynamic key access on Metadata still works FAILED
Caused by: groovy.lang.MissingPropertyException at MetadataAccessSpec.groovy:10
MetadataAccessSpec > map style get on Metadata still works FAILED
Caused by: groovy.lang.MissingMethodException at MetadataAccessSpec.groovy:15
3 tests completed, 2 failed
```
The third feature (the supported typed accessor) passes.
### Real plugin affected
`org.grails.plugins:grails-web-console:7.1.0`, an unchanged plugin released for Grails 7, added to a stock 8.0.0-M5 web app as `runtimeOnly "org.grails.plugins:grails-web-console:7.1.0"`.
The plugin resolves, is discovered, and the app starts. `GET /console` then returns a 500:
```
ERROR o.g.web.errors.GrailsExceptionResolver : MissingPropertyException occurred when processing request: [GET] /console
No such property: app.grails.version for class: grails.util.Metadata. Stacktrace follows:
Caused by: groovy.lang.MissingPropertyException: No such property: app.grails.version for class: grails.util.Metadata
at org.grails.plugins.console.ConsoleController.index(ConsoleController.groovy:28) ~[grails-web-console-7.1.0.jar!/:na]
at org.grails.core.DefaultGrailsControllerClass$ReflectionInvoker.invoke(DefaultGrailsControllerClass.java:215) ~[grails-core-8.0.0-M5.jar!/:8.0.0-M5]
at org.grails.web.mapping.mvc.UrlMappingsInfoHandlerAdapter.handle(UrlMappingsInfoHandlerAdapter.groovy:130) ~[grails-web-url-mappings-8.0.0-M5.jar!/:8.0.0-M5]
```
### Expected Behaviour
An unchanged Grails 7 plugin reading a metadata key through the dynamic/map-style API keeps working on Grails 8, or the breaking change is documented in the Grails 8 upgrade guide with a stated migration path.
### Actual Behaviour
The access throws at runtime, and the removal is not mentioned anywhere in `grails-doc/src/en/guide/upgrading/upgrading80x.adoc`.
### Origin of the change
The two methods were deprecated on the 7.0 line:
```groovy
@Deprecated(since = '7.0', forRemoval = true)
Object get(Object key) {
getProperty(key.toString(), Object, null)
}
@Deprecated(since = '7.0', forRemoval = true)
Object getProperty(String propertyName) {
get(propertyName)
}
```
and removed in commit `2110c45f367b9955da734afcc5dfd56d133941f7` ("refactor!: remove deprecated classes and methods") from `grails-gradle/model/src/main/groovy/grails/util/Metadata.groovy`.
The deprecation itself was correct. The problem is the runtime-only failure mode for already-published binary plugins, combined with the absence of any upgrade-guide entry.
### Suggested resolution
Two options, not mutually exclusive:
1. Provide an opt-in compatibility shim in the same spirit as `grails { legacyCommandSupport = true }` (#16011) and the proposed `grails.legacy.holdersDuringDoWithSpring` (#16101), restoring dynamic/map-style key access on `Metadata` with a one-time deprecation warning, so unchanged Grails 7 plugins keep working during migration.
2. At minimum, document the removal in the Grails 8 upgrade guide, alongside the existing API-break notes from #15823, with the replacement being `getProperty(key, targetType, defaultValue)` / `navigate(...)` / `getOrDefault(key, default)`.
### Notes
This was found in a broader compatibility sweep of released Grails 7 plugins against 8.0.0-M5. Other plugins in the same sweep (`grails-export` 7.1.0, `grails-mail` 5.0.3, `grails-shiro` 6.0.0, `cache-ehcache` 5.0.0-RC1) load and function correctly, so plugin loading itself is working - this is specific to the removed `Metadata` accessors.
Contributor guide
Research direction
Start with grails-gradle/model/src/main/groovy/grails/util/Metadata.groovy and reproduce the two failing cases in MetadataAccessSpec against 8.0.0-M5. Read grails-doc/src/en/guide/upgrading/upgrading80x.adoc and the referenced compatibility issues before determining whether the accepted scope is a compatibility shim, upgrade documentation, or both; done means the runtime failure is covered by regression evidence and the migration path is explicit.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- groovy
- Domain
- backend, documentation
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100