Update patch handling for final request fields#114
Conversation
| // 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); | ||
|
|
There was a problem hiding this comment.
This feels different than what we talked about. Do you think the additional reflection here is needed?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I think copyFields is the way to go and less error prone.
There was a problem hiding this comment.
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
- Array elements getting duplicated
- 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
- 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.
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.