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

   ### Expected Behavior
   
   `render(someObject as JSON)` should serialize an ordinary `Serializable` 
Java bean. Practically every
   Java bean declares `private static final long serialVersionUID`, so this 
covers a very large share of
   all objects users pass to `as JSON`.
   
   ### Actual Behaviour
   
   The request fails with HTTP 500 as soon as the object graph reaches a 
`Serializable` Java bean — here
   Spring Security's `SimpleGrantedAuthority`:
   
   ```
   org.grails.web.converters.exceptions.ConverterException:
       Error converting Bean with class 
org.springframework.security.core.authority.SimpleGrantedAuthority
        at 
org.grails.web.converters.marshaller.json.GenericJavaBeanMarshaller.marshalObject(GenericJavaBeanMarshaller.java:87)
        at 
org.grails.web.converters.marshaller.json.CollectionMarshaller.marshalObject(CollectionMarshaller.java:43)
        ...
   Caused by: java.lang.IllegalArgumentException: non-null object for
       private static final long 
org.springframework.security.core.authority.SimpleGrantedAuthority.serialVersionUID
        at 
org.grails.web.converters.marshaller.json.GenericJavaBeanMarshaller.marshalObject(GenericJavaBeanMarshaller.java:74)
   ```
   
   **Root cause.** `GenericJavaBeanMarshaller.java:74` has its `&&` operands in 
the wrong order:
   
   ```java
   for (Field field : o.getClass().getDeclaredFields()) {
       int modifiers = field.getModifiers();
       if (field.canAccess(o) && Modifier.isPublic(modifiers) && 
!(Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers))) {
   ```
   
   Per its javadoc, `AccessibleObject.canAccess(obj)` throws 
`IllegalArgumentException` when the member
   is `static` and `obj` is non-null. Because `canAccess(o)` is evaluated 
**first**, the
   `Modifier.isStatic(...)` guard never gets the chance to short-circuit it, 
and the loop blows up on
   the first static field it meets — typically `serialVersionUID`.
   
   ### Suggested fix
   
   Swap the operands so the cheap modifier checks run first:
   
   ```java
   if (Modifier.isPublic(modifiers) && !(Modifier.isStatic(modifiers) || 
Modifier.isTransient(modifiers)) && field.canAccess(o)) {
   ```
   
   Two call sites (line numbers on `8.0.x`):
   
   - 
`grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/GenericJavaBeanMarshaller.java:74`
   - 
`grails-converters/src/main/groovy/org/grails/web/converters/marshaller/xml/GenericJavaBeanMarshaller.java:57`
   
   Verified by shadowing the class in the reproducer's `src/main/groovy` with a 
patched copy.
   
   ### Regression
   
   This is a regression introduced in the 8.x line, in commit
   
[`9e60b8a4de`](https://github.com/apache/grails-core/commit/9e60b8a4de10822ecb22d404a38e21840f5ab821)
   ("refactor: replace usages of deprecated external APIs"). `7.2.x` and 
earlier read:
   
   ```java
   if (field.isAccessible() && Modifier.isPublic(modifiers) && 
!(Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers))) {
   ```
   
   `isAccessible()` takes no argument and never throws, so the operand order 
was harmless there. The
   mechanical `isAccessible()` → `canAccess(o)` swap made the first operand 
throwing, which is what
   turns this into a hard failure.
   
   Worth noting while fixing: on `7.2.x` this branch was effectively dead code, 
because
   `field.isAccessible()` returns `false` for any field that was never 
`setAccessible(true)`-ed. So
   `canAccess(o)` is the right *intent* — it makes public fields on public 
classes actually serialize —
   it just has to be evaluated last.
   
   Present on `8.0.x`, `8.1.x` and `9.0.x`; the commit is contained in tags 
`v8.0.0-M2` through
   `v8.0.0-M6`. Not present on `7.x`.
   
   ### Steps To Reproduce
   
   ```bash
   git clone https://github.com/sbglasius/asjson-bug
   cd asjson-bug
   ./gradlew integrationTest
   ```
   
   `DemoControllerTest > simple GET on show` fails.
   
   Note this bug is **masked** by the companion issue: out of the box the 
request dies earlier, in
   `GroovyBeanMarshaller`, on the anonymous `UserDetails` class. Patch that 
first (or hand `as JSON` any
   `Serializable` bean with a public class) and you land on this one. The 
reproducer's README documents
   both and shows how to shadow the marshallers to step through them one at a 
time.
   
   ### 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
   
   ### Companion issue
   
   Masked by https://github.com/apache/grails-core/issues/16294 — fix that one 
first to reach this bug.
   


-- 
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]

Reply via email to