Skip to content

WW-5726 fix(rest): decline the in-place merge of a polymorphic REST body property - #1941

Merged
lukaszlenart merged 1 commit into
mainfrom
WW-5726
Sep 15, 2026
Merged

lukaszlenart merged 1 commit into
mainfrom
WW-5726

Conversation

@lukaszlenart

Copy link
Copy Markdown
Member

Fixes WW-5726

Problem

A property that is mergeable (@JsonMerge, or merge enabled by configuration), has a value type deserializer (@JsonTypeInfo on the property or its declared type) and a non-null initial value is deserialized by Jackson past both authorizing wrappers. BeanDeserializerBase.resolve() builds a MergingSettableBeanProperty around the already-wrapped property; with a non-null current value it calls the final SettableBeanProperty#deserializeWith, which resolves a fresh deserializer for the existing value's class and deserializes into it in place. Neither AuthorizingSettableBeanProperty.deserializeAndSet nor AuthorizingValueDeserializer runs and set() is never called: the property binds unchecked and its members are authorized as the enclosing bean's own members, so a grant on a sibling of the same name authorizes the nested write. No type id is needed in the body — the subtype comes from the existing value.

Sibling of WW-5725, which covered the other set() and value-deserializer entry points.

Change

AuthorizingValueDeserializer now carries the property's value TypeDeserializer and answers supportsUpdate() with FALSE when one is present. resolve() asks the value deserializer before it builds the merging wrapper — through whichever property wrapper it has put around the authorizing one by then (ManagedReferenceProperty, ObjectIdReferenceProperty included, since they carry the value deserializer through) — so the property stays on the ordinary authorized path: it is checked itself, its members under its own prefix, and the value is replaced rather than merged.

  • Under Jackson's default MapperFeature.IGNORE_MERGE_FOR_UNMERGEABLE the declined merge is silently ignored; an application that disabled that feature gets Jackson's own InvalidDefinitionException ("values of type X cannot be merged").
  • A non-polymorphic merge keeps merging (WW-5725 path, testMergeIntoExistingValueIsAuthorized unchanged).
  • A WARN at resolve time (once per mapper and bean type) makes the disabled merge visible.

Hiding the merge info from the wrapper's getMetadata() was tried first and rejected in review: ManagedReferenceProperty and ObjectIdReferenceProperty copy the metadata field before resolve() consults it, so @JsonManagedReference or @JsonIdentityInfo on the polymorphic type kept the bypass open. Both are now regression tests.

Compatibility

This is decided when the deserializer is built, so it applies to every JacksonJsonHandler/JacksonXmlHandler user, not only when struts.parameters.requireAnnotations=true. An application that merges into a polymorphic property with a non-null initial value through the REST plugin must now send the type id in the body, and the existing value's state is replaced, not merged. No other property kind is affected. Version Notes entry to follow.

Tests

ParameterAuthorizingModuleTest: the ticket scenario (sibling grant no longer reaches the merged subtype), the @JsonIdentityInfo and @JsonManagedReference wrapper variants, the replace path through readerForUpdating, and the strict-mode refusal with IGNORE_MERGE_FOR_UNMERGEABLE disabled.

mvn test -DskipAssembly -pl plugins/rest: 184 tests, 0 failures.

🤖 Generated with Claude Code

@lukaszlenart
lukaszlenart marked this pull request as ready for review September 14, 2026 18:08
…ody property

A property that is mergeable (@JsonMerge or merge by configuration),
has a value type deserializer (@JsonTypeInfo) and a non-null initial
value is deserialized by Jackson past both authorizing wrappers.
MergingSettableBeanProperty is built by BeanDeserializerBase.resolve()
around the already-wrapped property and calls the final
SettableBeanProperty#deserializeWith, which resolves a fresh
deserializer for the existing value's class and deserializes into it
in place, so neither deserializeAndSet nor AuthorizingValueDeserializer
runs and set() is never called. The property binds unchecked and its
members are authorized as the enclosing bean's own members.

AuthorizingValueDeserializer now carries the property's value type
deserializer and answers supportsUpdate() with FALSE when one is
present. resolve() asks the value deserializer before it builds the
merging wrapper, through whichever property wrapper it has put around
the authorizing one by then, so the property stays on the ordinary
authorized path: it is checked itself, its members under its own
prefix, and the value is replaced rather than merged. Under Jackson's
default IGNORE_MERGE_FOR_UNMERGEABLE the declined merge is ignored; an
application that disabled the feature gets Jackson's bad-definition
report. A non-polymorphic merge keeps merging; WW-5725 covers that
path. A WARN at resolve time makes the disabled merge visible, since
the body now has to carry the type id.

Hiding the merge info from the wrapper's getMetadata() was tried first
and rejected: ManagedReferenceProperty and ObjectIdReferenceProperty
copy the metadata field before resolve() consults it, so a
@JsonManagedReference or @JsonIdentityInfo on the polymorphic type
kept the bypass open. SettableBeanProperty has no withMetadata, so the
merge cannot be stripped from the definition in updateBuilder either,
and refusing the definition outright would take the whole bean type
down for applications that never enabled requireAnnotations, since the
module is registered on every Jackson handler. Intercepting the typed
merge from RedactionAwareDeserializer#createContextual was rejected as
the ticket anticipated: the same call serves the ordinary resolution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@lukaszlenart
lukaszlenart merged commit 08b6fbd into main Sep 15, 2026
13 checks passed
@lukaszlenart
lukaszlenart deleted the WW-5726 branch September 15, 2026 05:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant