A Guide to JSON Patch Integration in Spring Apps
What is JSON Patch?
JSON Patch is a format specified in RFC 6902 that outlines modifications to a JSON document. It enables expressing a series of operations to apply to a JSON document, making it suitable for use with the HTTP PATCH method.
Let's take as an example the following JSON document. This document defines the typical User model used by most applications.
{
"email": "john@email.com",
"name": "Bruce Wayne",
"mobilePhoneNumber": "+35191919191",
"landlinePhoneNumber": "+3511010101010",
"address": "Street something 99"
}
If we want to represent changes to this document we can use JSON Patch.
[
{ "op": "remove", "path": "/phoneNumber" },
{ "op": "add", "path": "/alias", "value": "Batman" },
{ "op": "replace", "path": "/address", "value": "batcave" },
]
The previous example represents a set of changes to the original document. This can be very useful for updating operations on REST APIs. With JSON Patch we only send to the server a smaller JSON document that represents the changes we want to make to the original resource.
JSON Patch operations
Each operation consists of the following members:
op This is a mandatory parameter, it defines the operation we want to perform. Its value MUST be one of "add", "remove", "replace", "move", "copy", or "test"
path This is also a mandatory parameter. It defines the target of the operation. From now on we will refer to this field as the "target location"
value This parameter can be optional depending on the operation. It contains the value of the operation.
add
[
{ "op": "add", "path": "/alias", "value": "Batman" },
]
Adds a new member to the JSON document. In this case it will create the alias property on the document.
remove
[
{ "op": "remove", "path": "/phoneNumber" },
]
Removes the value at the target location.
replace
[
{ "op": "replace", "path": "/address", "value": "batcave" },
]
Updates the element at the target location to have the new value.
move
[
{ "op": "move", "from": "/mobilePhoneNumber", "path": "/landlinePhoneNumber" }
]
This operation is the equivalent of a remove followed by an add. In this case, it will move the value of the mobilePhoneNumber to the landlinePhoneNumber field.
copy
[
{ "op": "copy", "from": "/mobilePhoneNumber", "path": "/landlinePhoneNumber" }
]
This operation copies the value of the "from" field to the target field.
JSON-P: Java API for JSON Processing 1.1 - JSR 374
The Java API for JSON Processing provides portable APIs to parse, generate, transform, and query JSON. It introduced in Java world the following classes:
Class | Description |
Json | This is a Factory class for creating JSON processing objects. Contains static methods to create JSON readers, writers, builders, and their factory objects. |
JsonReader | This class reads JSON data from an input source and creates an in-memory object model. |
JsonValue | Represents a 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 |
JsonPatch | The implementation of JSON patch explained before |
Note: Java Introduced the JsonPatch in the JSR 374 . This JSR is the evolution of JSR 353, among other changes it added the support for JsonPointer and JsonPatch.
These classes provide a full representation of a JSON document and enable us to programmatically manipulate and create JSON documents.
Use JSON Patch in a Spring Controller
The first thing to do is to add the JSR 374 API to our project dependencies.
compile group: 'javax.json', name: 'javax.json-api', version: '1.1.4'
Since the javax.json package only defines the API, we need to also include an implementation. For this tutorial, I'll use Apache Johnzon
compile group: 'org.apache.johnzon', name: 'johnzon-core', version: '1.2.3'
Now we can create a new endpoint that receives a JsonPatch object.
@PatchMapping(value = "/{email}", consumes = "application/json-patch+json")
@Operation(description = "Updates an existing user", summary = "Updates an existing user")
@ApiResponses(value = {
@ApiResponse(responseCode = "202", description = "The user has been updated", content = @Content)})
@ResponseStatus(HttpStatus.ACCEPTED)
public void updateUser(@Parameter(description = "The user email address") @PathVariable String email, @RequestBody JsonPatch patchDocument) {
userService.patchUser(patchDocument, email);
}
The important thing to notice here is that the RequestBody is of type JsonPatch. Unfortunately Spring doesn't provide by default any converter for JsonPatch, so we have to implement a custom HttpMessageConverter.
import javax.json.Json;
import javax.json.JsonPatch;
import javax.json.JsonReader;
import org.apache.commons.lang3.NotImplementedException;
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;
@Component
public class JsonPatchHttpMessageConverter extends AbstractHttpMessageConverter<JsonPatch> {
public JsonPatchHttpMessageConverter() {
super(MediaType.valueOf("application/json-patch+json"));
}
@Override
protected JsonPatch readInternal(Class<? extends JsonPatch> clazz, HttpInputMessage inputMessage) throws HttpMessageNotReadableException {
try (JsonReader reader = Json.createReader(inputMessage.getBody())) {
return Json.createPatch(reader.readArray());
} catch (Exception e) {
throw new HttpMessageNotReadableException(e.getMessage(), inputMessage);
}
}
@Override
protected void writeInternal(JsonPatch jsonPatch, HttpOutputMessage outputMessage) throws HttpMessageNotWritableException {
throw new NotImplementedException("The write Json patch is not implemented");
}
@Override
protected boolean supports(Class<?> clazz) {
return JsonPatch.class.isAssignableFrom(clazz);
}
}
Since I don't want to serialize objects to JsonPatch objects the method writeInternal throws a NotImplementedException.
Now Spring can deserialize the received JSON document to a JsonPatch instance. With this JsonPatch instance, we can implement our service that will get the original user from the database and apply the requested changes.
public void patchUser(JsonPatch patchDocument, String email) {
//Gets the original user from the database
User originalUser = getUserByEmail(email); //1
logger.debug("original user {}", originalUser);
//Converts the original user to a JsonStructure
JsonStructure target = objectMapper.convertValue(originalUser, JsonStructure.class); //2
//Applies the patch to the original user
JsonValue patchedUser = patchDocument.apply(target); //3
//Converts the JsonValue to a User instance
User modifiedUser = objectMapper.convertValue(patchedUser, User.class); //4
logger.debug("modified user {}", modifiedUser);
//Saves the modified user in the database
usersList.put(email, modifiedUser); //5
}
As we can see the code to apply the patch is pretty straightforward. Let's see it step by step.
We get the original/unmodified user from the database.
Convert the user to a JsonStructure using the objectMapper. As said before, the JsonStructure was introduced by JSR-374 and among other classes enables us to programmatically manipulate JSON documents.
Apply the patch to the original user, we then get a JsonValue that represents the user with the changes present in the patch.
Convert the JsonValue to a user instance that we can store again on the database.
Store the modified user on the database.
With this implementation we can call the service like this:
curl -X PATCH "http://localhost:8080/users/bruce.wayne%40gotham.com" -H "accept: */*" -H "Content-Type: application/json-patch+json" -d "[{\"op\":\"replace\",\"path\":\"/email\",\"value\":\"batman@gotham.com\"}]"
There is a final step to make the previous code work. The Jackson Object mapper isn't capable of serializing/deserializing the new types defined in JSR-353/JSR-374, like the JsonValue and JsonStructure. To configure Jackson for these new types we must first add the following dependency to the project:
compile group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr353', version: '2.10.2'
Then we need to create a custom ObjectMapper bean and register the Jackson JSR-353 datatype as a module. In Spring we can create a configuration class to define a new bean:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr353.JSR353Module;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ApplicationConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JSR353Module());
return objectMapper;
}
}
Obviously this is a simplified example. On the service we should validate the requested changes, for example we may want to disallow the change of the user email. On a real application we would also need to validate permissions and business rules before making the update.
The complete code can be found at https://github.com/hugo-ma-alves/json-patch-demo. This repository contains a spring boot application, you just have to run it and then the swagger-ui interface to test the request becomes available at http://localhost:8080/swagger-ui/index.html.
Subscribe to my newsletter
Read articles from Hugo Alves directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Hugo Alves
Hugo Alves
My name is Hugo Alves and I am a Software Developer from Lisbon. I have been professionally writing code since 2013. I work mainly with Java backend applications, typically using Spring Boot with the traditional Servlet Stack, Kafka, Docker, REST APIs and so on. I also like to code in Python, and more recently GO. Apart from the coding part I’m also very interested by networking and security. The objective of this blog is to document some side projects I’m doing and everything I may think it can be useful or interesting.