sbglasius opened a new issue, #16294:
URL: https://github.com/apache/grails-core/issues/16294
### Expected Behavior
`render(someObject as JSON)` should serialize an object whose class is not
`public` — an anonymous
class, a local class, or a package-private class — as long as the property
getters themselves are
`public`. This is a very common shape in Spring Security code, where you
hand out an anonymous
implementation of a public interface such as `UserDetails`.
### Actual Behaviour
The request fails with HTTP 500:
```
org.grails.web.converters.exceptions.ConverterException:
Error converting Bean with class com.example.DemoController$1
at
org.grails.web.converters.marshaller.json.GroovyBeanMarshaller.marshalObject(GroovyBeanMarshaller.java:89)
at grails.converters.JSON.value(JSON.java:209)
at
org.grails.web.converters.marshaller.json.MapMarshaller.marshalObject(MapMarshaller.java:47)
...
Caused by: java.lang.IllegalAccessException:
class org.grails.web.converters.marshaller.json.GroovyBeanMarshaller
cannot access a member of class com.example.DemoController$1 with
modifiers "public"
at
java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:400)
at
java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:670)
at java.base/java.lang.reflect.Method.invoke(Method.java:556)
at
org.grails.web.converters.marshaller.json.GroovyBeanMarshaller.marshalObject(GroovyBeanMarshaller.java:69)
```
**Root cause.** `GroovyBeanMarshaller.java:69` invokes the read method with
no accessibility
handling at all:
```java
Object value = readMethod.invoke(o, (Object[]) null);
```
`BeanUtils.getPropertyDescriptors(clazz)` returns, for the *overridden*
properties, read methods
whose declaring class is the anonymous class itself. An anonymous class is
effectively
package-private, so a caller in another package cannot invoke those methods
even though the methods
carry the `public` modifier. `setAccessible`/`makeAccessible` is never
called — grepping
`grails-converters` for `makeAccessible|setAccessible` returns nothing.
Note the asymmetry this produces. Reproduced standalone against the
reproducer app's own classpath,
the `UserDetails` **default** methods keep the interface as their declaring
class and succeed, while
the three overridden ones fail:
```
clazz=com.example.probe.Factory$1 public=false
accountNonExpired: declared on ...UserDetails -> OK (true)
accountNonLocked: declared on ...UserDetails -> OK (true)
authorities: declared on ...Factory$1 ->
IllegalAccessException
credentialsNonExpired: declared on ...UserDetails -> OK (true)
enabled: declared on ...UserDetails -> OK (true)
password: declared on ...Factory$1 ->
IllegalAccessException
username: declared on ...Factory$1 ->
IllegalAccessException
getInterfaceMethodIfPossible -> UserDetails => OK (user)
makeAccessible -> OK (user)
```
### Suggested fix
Resolve the read method to a publicly accessible interface method, and make
it accessible:
```java
Method invokable = ClassUtils.getInterfaceMethodIfPossible(readMethod,
clazz);
ReflectionUtils.makeAccessible(invokable);
Object value = invokable.invoke(o, (Object[]) null);
```
All four marshallers call `readMethod.invoke` bare and need the same
treatment (line numbers on
`8.0.x`):
-
`grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/GroovyBeanMarshaller.java:69`
-
`grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/GenericJavaBeanMarshaller.java:67`
-
`grails-converters/src/main/groovy/org/grails/web/converters/marshaller/xml/GroovyBeanMarshaller.java:69`
-
`grails-converters/src/main/groovy/org/grails/web/converters/marshaller/xml/GenericJavaBeanMarshaller.java:49`
I verified the fix by shadowing the marshaller classes in the reproducer's
`src/main/groovy` with
patched copies: the request then returns HTTP 200 and the integration test
passes.
Fixing this bug alone is **not** sufficient to make the reproducer pass — it
then hits a second,
independent bug in `GenericJavaBeanMarshaller`, filed separately as the
companion issue.
### Affected versions
This is not a regression. The bare `readMethod.invoke(...)` call is present
on every branch that
carries the `grails-converters` module — `7.0.x`, `7.1.x`, `7.2.x`, `8.0.x`,
`8.1.x` and `9.0.x`.
I reproduced it on 8.0.0-M5 only; the other branches are identified by code
inspection.
### Steps To Reproduce
```bash
git clone https://github.com/sbglasius/asjson-bug
cd asjson-bug
./gradlew integrationTest
```
`DemoControllerTest > simple GET on show` fails. The controller action is
simply:
```groovy
def show() {
def user = new UserDetails() {
@Override Collection<? extends GrantedAuthority> getAuthorities() {
[new SimpleGrantedAuthority('ROLE_ADMIN')] }
@Override String getPassword() { null }
@Override String getUsername() { "user" }
}
render([user: user] as JSON)
}
```
`DemoControllerTest > simple GET on index`, which renders a plain `Map`,
passes — so the failure is
specific to marshalling the non-public class.
### Environment Information
- Operating System: Linux (kernel 6.17)
- JDK Version: 25.0.4 (BellSoft Liberica)
- Groovy 5.0.8, Spring Boot 4.1.0, Spring Framework 7.0.8, Spring Security
7.1.0
- Gradle 9.6
### Example Application
https://github.com/sbglasius/asjson-bug
### Version
8.0.0-M5
--
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: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]