apache / apache/grails-core

Grails 7 plugins break at runtime after removal of Metadata dynamic and map-style key access

Open
#16,122 3 comments 0 reactions 0 assignees View on GitHub
relates-to: external-plugin
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.