Patch http verb (Spring Boot)
Introduction
It covers HTTP PATCH operations with JSON Patch and JSON Merge Patch, in the context of Spring Boot.
JSON Merge Patch is a format that describes the changes to be made to a target JSON document using a syntax that closely mimics the document being modified.
JSON Patch is a format for expressing a sequence of operations to be applied to a JSON document.
Remark: JsonPatch validations are a nightmare, so the workaround with Optional should fit most use cases. See:
https://sites.google.com/site/pawneecity/sprint-boot/json-serialization-spring-boot#h.nl9kmojr2hx5
References
Using HTTP PATCH in Spring (10 June 2019, accessed 2024-apr-27)
https://www.student.unsw.edu.au/how-do-i-cite-electronic-sources
JSON Merge Patch (October 2014, accessed 2024-apr-27)
https://datatracker.ietf.org/doc/html/rfc7396
What is JSON Patch?
Using JSON Patch in Spring REST APIs
https://www.baeldung.com/spring-rest-json-patch
JSON Patch (RFC 6902)
https://datatracker.ietf.org/doc/html/rfc6902
JSON Pointer (RFC 6901)
https://datatracker.ietf.org/doc/html/rfc6901
JSON-P: Java API for JSON Processing
JSON-P 1.0 (aka Java API for JSON Processing 1.0) defined in JSR 253 brought support for JSON processing in Java EE.
JSON-P 1.1 defined in JSR 374 introduced support for JSON Patch and JSON Merge Patch formats to Java EE.
Type Description
Json Factory class for creating JSON processing objects
JsonPatch Represents an implementation of JSON Patch
JsonMergePatch Represents an implementation of JSON Merge Patch
JsonValue Represents an immutable JSON value that can be an object, an array, a number, a string, true, false or null
JsonStructure Super type for the two structured types in JSON: object and array
JSON-P is an API (set of interfaces). For working with it, an implementation is needed, eg: Apache Johnzon:
<dependency>
<groupId>org.apache.johnzon</groupId>
<artifactId>johnzon-core</artifactId>
<version>${johnzon.version}</version>
</dependency>
JSON Merge Patch
JSON Merge Patch is a format that describes the changes to be made to a target JSON document using a syntax that closely mimics the document being modified. It is defined in the RFC 7396 is identified by the application/merge-patch+json media type.
JSON Pointer & Operations
JSON Pointer (used by JSON Patch)
JSON Pointer (IETF RFC 6901) defines a string format for identifying a specific value within a JSON document. It is used by all operations in JSON Patch to specify the part of the document to operate on.
"" points to the root of the document
/ points to a key of "" on the root
In the following JSON:
{
"biscuits": [
{ "name": "Digestive" },
{ "name": "Choco Leibniz" }
]
}
/biscuits would point to the array of biscuits and
/biscuits/1/name would point to "Choco Leibniz"
If you need to refer to a key with ~ or / in its name, you must escape the characters with ~0 and ~1 respectively. For example, to get "baz" from { "foo/bar~": "baz" } you’d use the pointer /foo~1bar~0.
Finally, if you need to refer to the end of an array you can use - instead of an index. For example, to refer to the end of the array of biscuits above you would use /biscuits/-. This is useful when you need to insert a value at the end of an array.
Operations (used by JSON Patch)
Add
{ "op": "add", "path": "/biscuits/1", "value": { "name": "Ginger Nut" } }
Adds a value to an object or inserts it into an array. In the case of an array, the value is inserted before the given index. The - character can be used instead of an index to insert at the end of an array.
Remove
{ "op": "remove", "path": "/biscuits" }
Removes a value from an object or array.
{ "op": "remove", "path": "/biscuits/0" }
Removes the first element of the array at biscuits (or just removes the “0” key if biscuits is an object)
Replace
{ "op": "replace", "path": "/biscuits/0/name", "value": "Chocolate Digestive" }
Replaces a value. Equivalent to a “remove” followed by an “add”.
Copy
{ "op": "copy", "from": "/biscuits/0", "path": "/best_biscuit" }
Copies a value from one location to another within the JSON document. Both from and path are JSON Pointers.
Move
{ "op": "move", "from": "/biscuits", "path": "/cookies" }
Moves a value from one location to the other. Both from and path are JSON Pointers.
Test
{ "op": "test", "path": "/best_biscuit/name", "value": "Choco Leibniz" }
Tests that the specified value is set in the document. If the test fails, then the patch as a whole should not apply.
pom.xml
There is and interface and and implementation.
The interface is javax.json:javax.json-api (no need to add any explicit dependency)
The implementation requires to add a dependency that implements the interface:
<dependency>
<!-- JSON Patch implementation of interface javax.json:javax.json-api -->
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
<version>1.13</version>
</dependency>
Note: Another implementation (don't use both at the same time) could be:
<dependency>
<groupId>org.apache.johnzon</groupId>
<artifactId>johnzon-core</artifactId>
<version>1.2.14</version>
</dependency>
Sample DTO
public class Customer {
private String id;
private String telephone;
private List<String> favorites;
private Map<String, Boolean> communicationPreferences;
}
A JSON representation of an object of the class is:
{
"id":"1",
"telephone":"001-555-1234",
"favorites":["Milk","Eggs"],
"communicationPreferences": {"post":true, "email":true}
}
Sample Controller
Remark: The “Content-Type” must be: application/json-patch+json
@PatchMapping(path = "/{id}", consumes = "application/json-patch+json")
public ResponseEntity<Customer> updateCustomer(@PathVariable String id, @RequestBody JsonPatch patch) {
try {
Customer customer = customerService.findCustomer(id).orElseThrow(CustomerNotFoundException::new);
Customer customerPatched = applyPatchToCustomer(patch, customer);
customerService.updateCustomer(customerPatched);
return ResponseEntity.ok(customerPatched);
} catch (JsonPatchException | JsonProcessingException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
} catch (CustomerNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
}
The real job (usually in a 'Service') would make all kinds of validations on JsonPatch, etc:
private Customer applyPatchToCustomer(
JsonPatch patch, Customer targetCustomer) throws JsonPatchException, JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode patched = patch.apply(objectMapper.convertValue(targetCustomer, JsonNode.class));
return objectMapper.treeToValue(patched, Customer.class);
}
Sample invocations
Create
curl -i -X POST http://localhost:8080/customers -H "Content-Type: application/json"
-d '{"telephone":"+1-555-12","favorites":["Milk","Eggs"],"communicationPreferences":{"post":true,"email":true}}'
Patch
curl -i -X PATCH http://localhost:8080/customers/1 -H "Content-Type: application/json-patch+json" -d '[
{"op":"replace","path":"/telephone","value":"+1-555-56"},
{"op":"add","path":"/favorites/0","value": "Bread"}
]'
Patch w/ the map 'communicationPreferences'
curl -i -X PATCH http://localhost:8080/customers/1 -H "Content-Type: application/json-patch+json" -d '[
{"op":"replace","path":"/communicationPreferences","value": {"post":false, "email":false}},
{"op":"remove","path":"/communicationPreferences/email"}
]'
Issue referencing a single key of a map (it says it's fixed):
https://github.com/spring-projects/spring-data-rest/issues/1697