WW-5726 fix(rest): decline the in-place merge of a polymorphic REST body property - #1941
Merged
Merged
Conversation
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>
lukaszlenart
force-pushed
the
WW-5726
branch
from
September 14, 2026 18:57
885cadf to
f22f8c9
Compare
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes WW-5726
Problem
A property that is mergeable (
@JsonMerge, or merge enabled by configuration), has a value type deserializer (@JsonTypeInfoon the property or its declared type) and a non-null initial value is deserialized by Jackson past both authorizing wrappers.BeanDeserializerBase.resolve()builds aMergingSettableBeanPropertyaround the already-wrapped property; with a non-null current value it calls thefinalSettableBeanProperty#deserializeWith, which resolves a fresh deserializer for the existing value's class and deserializes into it in place. NeitherAuthorizingSettableBeanProperty.deserializeAndSetnorAuthorizingValueDeserializerruns andset()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
AuthorizingValueDeserializernow carries the property's valueTypeDeserializerand answerssupportsUpdate()withFALSEwhen 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,ObjectIdReferencePropertyincluded, 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.MapperFeature.IGNORE_MERGE_FOR_UNMERGEABLEthe declined merge is silently ignored; an application that disabled that feature gets Jackson's ownInvalidDefinitionException("values of type X cannot be merged").testMergeIntoExistingValueIsAuthorizedunchanged).Hiding the merge info from the wrapper's
getMetadata()was tried first and rejected in review:ManagedReferencePropertyandObjectIdReferencePropertycopy the metadata field beforeresolve()consults it, so@JsonManagedReferenceor@JsonIdentityInfoon 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/JacksonXmlHandleruser, not only whenstruts.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@JsonIdentityInfoand@JsonManagedReferencewrapper variants, the replace path throughreaderForUpdating, and the strict-mode refusal withIGNORE_MERGE_FOR_UNMERGEABLEdisabled.mvn test -DskipAssembly -pl plugins/rest: 184 tests, 0 failures.🤖 Generated with Claude Code