borinquenkid commented on PR #16414:
URL: https://github.com/apache/grails-core/pull/16414#issuecomment-5882291941

   Reviewed at head `95066b7b83`. I compiled it and reran the claimed test 
counts myself (`grails-web-common` 113, `grails-converters` 205, 
`grails-views-gson` 185 — all green, matching the PR body), then tried to break 
the design rather than just confirm it. Two findings.
   
   ### 1. `formatKey()` depends on a Jackson method documented as test-only
   
   `JsonMapperSupport.formatKey()` calls `mapper._serializationContext()`. In 
Jackson 3.1.6's own source (`ObjectMapper.java`):
   
   ```java
   // NOTE: only public to allow for testing
   public SerializationContextExt _serializationContext() {
   ```
   
   and `SerializationContextExt`'s class Javadoc: "adds methods needed by 
`ObjectMapper`  ... but that are not to be exposed as general context during 
serialization." That's Jackson's own maintainers saying this isn't meant to be 
built on. It works today, but Jackson doesn't consider changing or removing it 
a breaking change — there's no compatibility guarantee across even a patch 
release.
   
   ### 2. A custom Jackson serializer can silently bypass a registered Grails 
marshaller
   
   `JsonMapperSupport.rendersValue()` decides Jackson "owns" a type by checking 
its serializer isn't `instanceof` one of six internal classes 
(`BeanSerializerBase`, `EnumSerializer`, `ReferenceTypeSerializer`, 
`StdContainerSerializer`, `UnsupportedTypeSerializer`, 
`ToEmptyObjectSerializer`). But Jackson's own docs recommend `StdSerializer` as 
the base class for a custom serializer — and it extends `ValueSerializer` 
directly, none of those six. So any textbook custom serializer an app or a 
plugin registers passes the check and is treated as fully owned by Jackson.
   
   Repro, against this branch:
   
   ```groovy
   def module = new SimpleModule()
   module.addSerializer(Wrapper, new StdSerializer<Wrapper>(Wrapper) {
       void serialize(Wrapper value, JsonGenerator gen, SerializationContext 
ctxt) {
           gen.writeStartObject()
           gen.writeName('inner')
           gen.writePOJO(value.inner)   // not converter.convertAnother()
           gen.writeEndObject()
       }
   })
   def mapper = JsonMapper.builder().addModule(module).build()
   // ... registered as the app's JsonMapper bean ...
   
   JSON.registerObjectMarshaller(Secret) { Secret s -> '***REDACTED***' }
   
   new JSON([w: new Wrapper(inner: new Secret(value: 
'super-secret'))]).toString()
   ```
   
   Expected (what an app relying on `registerObjectMarshaller` to redact 
`Secret` would expect):
   ```
   {"w":{"inner":"***REDACTED***"}}
   ```
   
   Actual:
   ```
   {"w":{"inner":{"value":"super-secret"}}}
   ```
   
   Once `JsonMapperValueMarshaller` hands `Wrapper` to `generator.writePOJO()`, 
the custom serializer runs inside Jackson's own machinery. Its 
`gen.writePOJO(value.inner)` call never comes back through 
`converter.convertAnother()`, so Grails never gets the chance to apply the 
`Secret` marshaller — Jackson's default bean serializer renders it raw instead.
   
   This is new exposure, not a pre-existing gap: before this PR a bare Jackson 
module had zero effect on `grails.converters.JSON` — `registerObjectMarshaller` 
was the only thing that controlled rendering. Now a plugin's or an app's own 
Jackson module can silently override an app's redaction/transform marshaller 
for a nested type it has no idea about, with no error or warning. It needs both 
ingredients (a custom serializer that embeds another value, and a separate 
marshaller registered for that embedded type) so it won't hit every app, but 
the failure mode is exactly the one that matters when it does.
   
   ### A question on the overall approach
   
   Given `spring.jackson.*` and the auto-configured `JsonMapper` are meant to 
be used directly (`jsonMapper.writeValueAsString(value)`), why keep this hybrid 
split — Grails marshals containers/beans/domain objects, Jackson marshals 
"simple values" it recognizes — instead of just handing the whole object graph 
to the `JsonMapper` and letting Jackson serialize it end to end, the standard 
documented pattern? I'd guess it's to keep `ObjectMarshaller<JSON>` / 
`JSON.registerObjectMarshaller` working, and Grails' GORM-aware domain-class 
rendering (proxies, circular references, includes/excludes, `deep` vs. shallow 
associations) — none of which map onto plain Jackson bean serialization. If 
that's the reasoning, it's worth saying explicitly in the PR description, since 
it's exactly the seam the bypass above lives in.


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