Skip to content

Update patch handling for final request fields#114

Closed
hjaret wants to merge 1 commit into
mainfrom
jaret/ENG-3921/patch-updates
Closed

Update patch handling for final request fields#114
hjaret wants to merge 1 commit into
mainfrom
jaret/ENG-3921/patch-updates

Conversation

@hjaret
Copy link
Copy Markdown
Contributor

@hjaret hjaret commented May 5, 2026

Problem:
If an action's request field set as final, when PrimeMVC handles json-patch+json and merge-patch+json after performing the patch it replaces the request field with the patched object which fails because the field is final.

Solution:
If the field is final, it is now being merged in-place to avoid the illegal access exception.

Copy link
Copy Markdown
Contributor

@wied03 wied03 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question

// This stays with the patch behavior we were using before and we can
// continue assigning this object on the request for non-final members.
Object patchedObject = objectMapper.readerFor(requestMember.type).readValue(patched);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels different than what we talked about. Do you think the additional reflection here is needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're referring to the copyFields stuff it is different. There's some quirkiness around readerForUpdating that I'm having trouble getting around. The best I can do is

// Build the patch from the incoming request body
    Patch patch = objectMapper.readerFor(contentType.equals("application/json-patch+json")
                                             ? JsonPatch.class
                                             : JsonMergePatch.class)
                              .readValue(request.getInputStream());

    // Patch the current object
    JsonNode source = objectMapper.valueToTree(currentValue);
    JsonNode patched = patch.apply(source);
    Object patchedObject = objectMapper.readerFor(requestMember.type).readValue(patched);

    // Prefer replacement when possible.
    if (!isFinalRequestMember(action.getClass(), requestMember.name)) {
      expressionEvaluator.setValue(requestMember.name, action, patchedObject);
      return;
    }

    ObjectMapper updateMapper = objectMapper.copy();
    // Include properties even if null; if they are missing in readerForUpdating they values can remain
    // unchanged instead of being cleared
    updateMapper.setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.ALWAYS);
    // Prevent list/map merge-append behavior during update
    updateMapper.setDefaultMergeable(false);

    // Update in place to support final fields
    updateMapper.readerForUpdating(currentValue).readValue(updateMapper.writeValueAsString(patchedObject));

and only apply the readerForUpdating logic when absolutely necessary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think copyFields is the way to go and less error prone.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using readerForUpdating to perform the in-place merge of the patched object into the current object is resulting in some undesired results.

The PATCH for both JSON Patch (RFC-6902) and Merge Patch (RFC-7386/RFC-7396) is performed by the patch.apply call.

The JSON Patch uses an array of operations to be performed to update the existing object.
The Merge Patch defines a partial object that is used to update the existing object.

Both of those produce a fully realized, and patched, JSON "object". When we then try and use that fully realized object in a Jackson readerForUpdating it is using a whole new set of rules to merge those objects together.

A couple things I was seeing

  1. Array elements getting duplicated
  2. If a JSON Patch removes a field, readerForUpdating won't remove or null it in the existing object because absence of a field means leave it as is
  3. I saw "null" as the only member of an array

What we really want after the PATCH has happened is to replace the existing object with the patched object, but in the case of a final field the best we can do is replace all of the top level fields of that final object with those from the patched object.

@hjaret hjaret closed this May 8, 2026
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.

2 participants