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
Uses JsonMergePatch with SpringBoot 2.4 and javax.json
https://github.com/omlip/json-patch-example
Very good examples using Spring controllers with JSON Patch and JSON Merge Patch (source code available on GitHub).
https://cassiomolin.com/programming/using-http-patch-in-spring/
https://datatracker.ietf.org/doc/html/rfc7396
https://www.baeldung.com/spring-rest-json-patch
https://datatracker.ietf.org/doc/html/rfc6902
https://datatracker.ietf.org/doc/html/rfc6901
https://johnzon.apache.org/index.html
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 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.
Reference fpdefense (sb-3 and jakarta.json)
Eg: For sb-2.4 and javax.json see reference "omlip/json-patch-example"
pom.xml (dependencies)
<!-- JSR-353 (JSON Processing): API [jakarta,JsonMergePatch] -->
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
</dependency>
<!-- JSR-353 (JSON Processing): Apache Johnzon implementation [jakarta,JsonMergePatch] -->
<dependency>
<groupId>org.apache.johnzon</groupId>
<artifactId>johnzon-core</artifactId>
<version>2.0.1</version>
</dependency>
<!-- Jackson module for the JSR-353 (JSON Processing) [jakarta, JsonMergePatch] -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jakarta-jsonp</artifactId>
</dependency>
src/main/java/mypackage/mergepatch/JsonMergePatchHelper.java
package mypackage.mergepatch;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsonp.JSONPModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import jakarta.json.JsonMergePatch;
import jakarta.json.JsonStructure;
import jakarta.json.JsonValue;
import lombok.experimental.UtilityClass;
/**
* Helper for JSON Merge Patch.
*
* JSON Merge Patch is defined in the RFC 7396 is identified by the application/merge-patch+json
* media type.
*/
@UtilityClass
public class JsonMergePatchHelper {
private static ObjectMapper objectMapper;
static {
objectMapper = new ObjectMapper();
objectMapper.registerModule(new JSONPModule()); // JSR353
objectMapper.registerModule(new JavaTimeModule()); // java.time.*
}
/**
* Apply patch to the target bean.
*
* @param targetBean The target bean
* @param patch Patch to apply
* @param <T> Type of the target Bean
* @return Target bean patched with patch
*/
@SuppressWarnings("unchecked")
public static <T> T mergePatch(T targetBean, JsonMergePatch patch) {
// Convert the Java bean to a JSON document
JsonStructure target = JsonMergePatchHelper.objectMapper.convertValue(targetBean,
JsonStructure.class);
// Apply the JSON Patch to the JSON document
JsonValue patched = patch.apply(target);
// Convert the JSON document to a Java bean and return it
return JsonMergePatchHelper.objectMapper.convertValue(patched, (Class<T>) targetBean
.getClass());
}
}
src/main/java/mypackage/mergepatch/JsonMergePatchHttpMessageConverter.java
package mypackage.mergepatch;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.stereotype.Component;
import jakarta.json.Json;
import jakarta.json.JsonMergePatch;
import jakarta.json.JsonReader;
import jakarta.json.JsonWriter;
import java.io.IOException;
/**
* HTTP message converter for controllers to be able to convert JSON string into JsonMergePatch
* object (RFC 7396).
*/
@Component
public class JsonMergePatchHttpMessageConverter extends
AbstractHttpMessageConverter<JsonMergePatch> {
public JsonMergePatchHttpMessageConverter() {
super(MediaType.valueOf("application/merge-patch+json"));
}
@Override
protected boolean supports(Class<?> clazz) {
return JsonMergePatch.class.isAssignableFrom(clazz);
}
@Override
protected JsonMergePatch readInternal(Class<? extends JsonMergePatch> aClass,
HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
try (JsonReader reader = Json.createReader(httpInputMessage.getBody())) {
return Json.createMergePatch(reader.readValue());
} catch (Exception e) {
throw new HttpMessageNotReadableException(e.getMessage(), httpInputMessage);
}
}
@Override
protected void writeInternal(JsonMergePatch jsonMergePatch, HttpOutputMessage httpOutputMessage)
throws IOException, HttpMessageNotWritableException {
try (JsonWriter writer = Json.createWriter(httpOutputMessage.getBody())) {
writer.write(jsonMergePatch.toJsonValue());
} catch (Exception e) {
throw new HttpMessageNotWritableException(e.getMessage(), e);
}
}
}
src/main/java/mypackage/mergepatch/package-info.java
/**
* <pre>
* (at)NonNullApi should be used at package level in association with
* org.springframework.lang.Nullable annotations at parameter and return value level.
*
* (at)NonNullFields should be used at package level in association with
* org.springframework.lang.Nullable annotations at field level.
* </pre>
*/
@NonNullApi
@NonNullFields
package mypackage.mergepatch;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;
src/main/java/mypackage/controller/JsonMergePatchSampleController.java
package mypackage.controller;
import jakarta.json.JsonMergePatch;
...
/**
* JsonMergePatch fully functional.
*
* @param id The UUID of an existing record
* @param patch The patch modification to apply
* @return The patched record
*/
@PatchMapping(path = "/app-request/{id}", consumes = "application/merge-patch+json")
public ResponseEntity<AppRequest> updateAppRequest(@Parameter(
description = "Request id (UUID v4)", required = true,
example = "104c2354-f84a-416b-a894-f8552ab9d1f5") @RequestParam(
name = "rId") @PathVariable UUID id,
@io.swagger.v3.oas.annotations.parameters.RequestBody(content = @Content(examples = {
@io.swagger.v3.oas.annotations.media.ExampleObject(name = "AppRequest sample",
summary = "App request sample", value = """
{
"createInstant": "2027-08-05T13:39:16.523Z"
}""") })) @RequestBody JsonMergePatch patch) {
// Find the model that will be patched
AppRequest r = this.appRequestService.find(id).orElseThrow();
// Apply the patch
AppRequest rPatched = JsonMergePatchHelper.mergePatch(r, patch);
// Persist the changes
AppRequest rSaved = this.appRequestService.save(rPatched);
// Return
return new ResponseEntity<>(rSaved, HttpStatus.OK);
}
Eg: Tweety Bird generic data
JsonMergePatchHelper.java
package mypackage.helper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.fge.jsonpatch.JsonPatchException;
import com.github.fge.jsonpatch.mergepatch.JsonMergePatch;
import lombok.experimental.UtilityClass;
import java.io.IOException;
/**
* Helper for JSON Merge Patch.
*
* JSON Merge Patch is defined in the RFC 7396 is identified by the application/merge-patch+json
* media type.
*/
@UtilityClass
public class JsonMergePatchHelper {
/***/
public static <T> T mergePatch(T t, String patch, Class<T> clazz) throws IOException,
JsonPatchException {
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.convertValue(t, JsonNode.class);
JsonNode patchNode = mapper.readTree(patch);
JsonMergePatch mergePatch = JsonMergePatch.fromJson(patchNode);
node = mergePatch.apply(node);
return mapper.treeToValue(node, clazz);
}
}
--
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.
{ "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.
{ "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)
{ "op": "replace", "path": "/biscuits/0/name", "value": "Chocolate Digestive" }
Replaces a value. Equivalent to a “remove” followed by an “add”.
{ "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.
{ "op": "move", "from": "/biscuits", "path": "/cookies" }
Moves a value from one location to the other. Both from and path are JSON Pointers.
{ "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.
Way too complex, JSON Merge Patch (RFC 7396) is preferred.
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>
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}
}
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);
}
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