You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@grails.apache.org by "jamesfredley (via GitHub)" <gi...@apache.org> on 2026/08/10 15:56:18 UTC

[I] Grails 7 plugins break at runtime after removal of Metadata dynamic and map-style key access [grails-core]

jamesfredley opened a new issue, #16122:
URL: https://github.com/apache/grails-core/issues/16122

   ### 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.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [I] Grails 7 plugins break at runtime after removal of Metadata dynamic and map-style key access [grails-core]

Posted by "jdaugherty (via GitHub)" <gi...@apache.org>.
jdaugherty commented on issue #16122:
URL: https://github.com/apache/grails-core/issues/16122#issuecomment-5281116238

   @matrei this was a deprecation removal.  If we're going to add shims for every deprecation removal, it seems like the framework won't be able to evolve.  


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [I] Grails 7 plugins break at runtime after removal of Metadata dynamic and map-style key access [grails-core]

Posted by "jamesfredley (via GitHub)" <gi...@apache.org>.
jamesfredley commented on issue #16122:
URL: https://github.com/apache/grails-core/issues/16122#issuecomment-5245025245

   ### Shim feasibility: this one looks small and self-contained
   
   Following the compatibility-shim precedents in #16011 (restore a deprecated contract behind an opt-in) and #16101 (narrow, default-off behavioural flag), I looked at what restoring these two access forms would actually require. It appears to be a very small change, because the class has already kept most of the Map-like surface.
   
   The runtime class is `grails/util/Metadata.groovy`, which lives in the **`grails-gradle/model`** build rather than in `grails-core` - worth noting up front, since that is a separately published artifact that `grails-core` consumes.
   
   Its current shape:
   
   ```groovy
   @CompileStatic
   class Metadata {
   
       <T> T getProperty(String key, Class<T> targetType, T defaultValue) {
           return this.propertyResolver.getProperty(key, targetType, defaultValue)
       }
   
       boolean containsKey(Object key) {
           return this.propertyResolver.containsProperty((String) key)
       }
   
       Object getOrDefault(Object key, Object defaultValue) {
           return getProperty(key.toString(), Object, defaultValue)
       }
   
       Object navigate(String... path) {
           return this.propertyResolver.getProperty(path.join('.').toString(), Object, null)
       }
   }
   ```
   
   So `containsKey(Object)` and `getOrDefault(Object, Object)` - both Map-style accessors - are still present and still delegate to the resolver. The class is not a `Map` (it does not implement `Map`), and what is missing is specifically:
   
   - `get(Object)` - which is what `Metadata.current.get('some.key')` binds to
   - a single-argument property read - which is what `Metadata.current.'some.key'` binds to
   
   Both have an exact one-line expression already in use elsewhere in the class, namely `getProperty(key.toString(), Object, defaultValue)`.
   
   Restoring them would therefore be roughly:
   
   ```groovy
   @Deprecated
   Object get(Object key) {
       return getProperty(key.toString(), Object, null)
   }
   
   @Deprecated
   Object propertyMissing(String name) {
       return getProperty(name, Object, null)
   }
   ```
   
   `propertyMissing` still works despite `@CompileStatic` on the class, because the failing call sites are dynamic Groovy in the *calling* plugin, so dispatch goes through the receiver's MetaClass at runtime.
   
   That would make both previously-working forms return the value or `null` again, rather than throwing, and it keeps the supported typed accessor `getProperty(key, Class, default)` as the documented path.
   
   ### Why this may be worth shimming rather than only documenting
   
   The two removed forms fail at **runtime**, not at build time, so an unchanged Grails 7 binary plugin resolves, loads, and boots before throwing on the first request that touches the code path - as `grails-web-console:7.1.0` does with a 500 on `GET /console`. Unlike the other Groovy-level breaks I have found in this sweep (#16123, #16126, #16128), this one does not require the plugin to be recompiled to be fixed; the framework can simply answer the call again.
   
   If restoring them unconditionally is not desired, the #16101 pattern would fit: keep them removed by default and allow opting in, so a legacy application can run today while plugin authors migrate to `getProperty(key, Class, default)`.
   
   I have not raised a PR for this - flagging the assessment in case it is useful.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [I] Grails 7 plugins break at runtime after removal of Metadata dynamic and map-style key access [grails-core]

Posted by "jamesfredley (via GitHub)" <gi...@apache.org>.
jamesfredley closed issue #16122: Grails 7 plugins break at runtime after removal of Metadata dynamic and map-style key access
URL: https://github.com/apache/grails-core/issues/16122


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [I] Grails 7 plugins break at runtime after removal of Metadata dynamic and map-style key access [grails-core]

Posted by "jamesfredley (via GitHub)" <gi...@apache.org>.
jamesfredley commented on issue #16122:
URL: https://github.com/apache/grails-core/issues/16122#issuecomment-5957720442

   The dynamic and map-style `Metadata` reads still fail on current 8.0.x (`d13aa3328220eba6c517d49a9569e4b091d646e0`), and that is the removal described here. `grails.util.Metadata` has no `get(Object)` and no single-argument `getProperty(String)`, so `Metadata.current.'some.key'` still raises `MissingPropertyException` and `Metadata.current.get('some.key')` still raises `MissingMethodException`. Those methods were removed in `2110c45f367b9955da734afcc5dfd56d133941f7` and were not restored. A compatibility shim was not added.
   
   The migration path in the expected behaviour is now in the Grails 8 upgrade guide, section 23.1, "Metadata dynamic and map-style access":
   
   https://grails.apache.org/docs/8.0.x/guide/upgrading.html#_23_1_metadata_dynamic_and_map_style_access
   
   Replace `Metadata.current.'info.app.grailsVersion'` and `Metadata.current.get('info.app.name')` with `Metadata.current.getProperty('info.app.grailsVersion', String, null)` and `Metadata.current.getProperty('info.app.name', String, null)`. Nested keys can use `Metadata.current.navigate('some', 'nested', 'key')`. A published plugin has to be rebuilt; there is no Metadata compatibility shim. That section landed in https://github.com/apache/grails-core/pull/16141 (`2c8b338f1ce`, 2026-08-12) and is in `v8.0.0-M6`, `v8.0.0-RC1`, and `v8.0.0-RC2`.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [I] Grails 7 plugins break at runtime after removal of Metadata dynamic and map-style key access [grails-core]

Posted by "jamesfredley (via GitHub)" <gi...@apache.org>.
jamesfredley commented on issue #16122:
URL: https://github.com/apache/grails-core/issues/16122#issuecomment-5243294166

   ### Ecosystem impact: narrower than the other two compatibility issues
   
   I scanned the released jars of 15 Grails plugins to see how many actually rely on the removed dynamic/map-style `Metadata` accessors, so this can be prioritised against the other findings.
   
   Detection note: my first attempt looked for a `grails/util/Metadata` class reference in the constant pool and produced a **false negative on the very plugin in this report** - `ConsoleController` contains the string `app.grails.version` but no `Metadata` class reference, because it obtains the metadata object indirectly rather than through a direct static call. So the reliable signal is the **metadata key string**, not the class reference. The results below use key strings, and they do correctly flag the known-failing plugin.
   
   | Plugin | Verdict |
   |---|---|
   | `org.grails.plugins:grails-web-console:7.1.0` | **LEGACY KEY** - `ConsoleController.class` contains `app.grails.version` |
   | `org.grails.plugins:grails-shiro:6.0.0` | weak only - `ShiroGrailsPlugin$_doWithSpring_closure2` references `app.name` / `info.app.` |
   | `audit-logging:6.0.0`, `grails-elasticsearch:5.1.0`, `grails-logical-delete:3.0.0`, `cache-ehcache:5.0.0-RC1`, `grails-cache-redis:6.0.0-RC1`, `grails-mail:5.0.3`, `grails-export:7.1.0`, `grails-quartz:4.0.1`, `grails-redis:5.0.1`, `sitemesh2:6.2.4`, `database-migration:9.0.0-M3`, `spring-security-core:7.0.0-M3`, `asynchronous-mail:3.1.2` | none |
   
   The Shiro result is a useful control: it reads metadata but through the supported accessor, and it loaded and wired correctly on 8.0.0-M5 in the same sweep. So "reads metadata" is not by itself a problem - only the removed dynamic/map-style access is.
   
   ### Takeaway
   
   Only one plugin in the sample is affected, so this is materially **lower impact** than the other two issues found in the same sweep (#16123 and #16124), and could reasonably be prioritised behind them.
   
   Two caveats before treating it as rare:
   
   - The heuristic detects literal metadata key strings. Code that builds a key dynamically, or reads a metadata value through some other indirection, would not be caught.
   - The failure mode remains the concerning part regardless of frequency: it is invisible at compile time and only appears when the code path is exercised at runtime, as a 500 with a `MissingPropertyException` that points at `grails.util.Metadata` rather than at the removal.
   
   Given that, documenting the removal in the upgrade guide may be worth doing even if a compatibility shim is judged unnecessary.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@grails.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org