Repository navigation
WW-5790 fix(rest): copy only what the body handler changed in the two-phase copy - #2051
Merged
Merged
Conversation
…-phase copy Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
…lass baseline Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
lukaszlenart
marked this pull request as ready for review
October 23, 2026 06:05
Contributor
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
5 open issues
This PR introduces a large internal implementation plan document underdocs/superpowers/plans/.… · New Returning null fromContentTypeHandler#fromObjectcan cause fragile tests (and potential NPEs) if… · NewbaselineValueis computed (via reflective getter invocation) even whensourceValueis null and… · New The skip-rule comment says \"leaf or collection\" but theObjects.deepEqualscheck is also… · NewbaselineValueis computed (via reflective getter invocation) even whensourceValueis null and… · New
What changed in this PR
Fixes WW-5790 by preventing ContentTypeInterceptor’s two-phase copy from overwriting pre-existing model values with constructor defaults when the request body omits fields (under struts.parameters.requireAnnotations=true and non-AuthorizationAwareContentTypeHandler handlers).
Changes:
- Add baseline instance creation in
applyTwoPhaseDeserializeand compare against it to copy only body-changed values. - Thread baseline through nested-bean recursion while keeping existing authorization checks and deep-copy behavior for collections/maps/arrays.
- Add integration tests (and fixtures) that cover omitted defaults, nested beans, subtype handling, and documented limits.
| File | Description |
|---|---|
| plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java | Introduces baseline comparison to avoid copying constructor defaults during two-phase deserialization. |
| plugins/rest/src/test/java/org/apache/struts2/rest/ContentTypeInterceptorIntegrationTest.java | Adds WW-5790 regression coverage via a scripted non-aware handler and new model fixtures. |
| docs/superpowers/plans/2026-10-23-WW-5790-two-phase-copy-baseline.md | Adds an implementation plan document describing the approach and steps. |
Review effort: Lite
…n needs it Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
Copilot stopped reviewing on behalf of
lukaszlenart due to an error
October 23, 2026 07:38
|
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-5790
What was wrong
With
struts.parameters.requireAnnotations=true, a content-type handler that does not implementAuthorizationAwareContentTypeHandlergoes through the two-phase copy inContentTypeInterceptor: a fresh instance is built with the no-arg constructor, the handler fills it, and every non-null authorized property is copied onto the real target. The copy could not tell a value the body supplied from a value the constructor assigned, so every property the body omitted but the constructor initialised was written over whatparamshad bound orprepare()had loaded. Scalars, constructor-initialised collections (an empty list replaced a populated one) and constructor-initialised nested beans (the recursion reset their leaves) all suffered. Follow-up to #2050, which covered the handlers that never read the body; this one coversXStreamHandlerand application-provided handlers.Change
applyTwoPhaseDeserializebuilds a second, untouched fresh instance as a baseline.copyAuthorizedPropertiestakes the baseline and skips a property whose filled valueObjects.deepEqualsthe baseline value. The baseline is threaded through the nested-bean recursion: the baseline's own nested value, else a fresh instance of the nested class, else null, which keeps today's copy-non-null rule for that subtree. Collection, map and array elements are new objects already, so their recursion passes no baseline.Documented limits
equals, and against a baseline of the handler's runtime type, so a subtype with extra properties and an entity with an id-onlyequalsboth bind correctly.UUID.randomUUID(),Instant.now()) always differ from the baseline and are still copied, as before.Behaviour changes only when all three hold:
requireAnnotations=true, a non-aware handler, and a property with a non-null constructor default. The default handlers are unaffected.Tests
Eleven tests in
ContentTypeInterceptorIntegrationTeston a ModelDriven fixture driven by a scripted non-aware handler: omitted scalar default kept, set scalar applied, omitted collection default kept, omitted nested default kept, partially set nested bean applies only the set leaf, nested type without a no-arg constructor still applies the set leaf, the documented limits pinned by name (body value equal to the default, primitive zero value, empty collection), a nested subtype whose own leaf is applied, and a nested bean whose id-onlyequalsdoes not hide a changed leaf. Five failed before the change with the constructor value in place of the pre-existing one; the subtype and partial-equals tests came out of the review and failed before their fix.Verification
plugins/restsuite with core compiled from the branch: 237 tests, 0 failures.🤖 Generated with Claude Code