Skip to content

Latest commit

History

81 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Using HTTP PATCH in Spring

This project demonstrates an approach to support HTTP PATCH with JSON Patch and JSON Merge Patch for performing partial modifications to resources in Spring, as described in my blog. I also have put together a Postman collection so you can play around with the API.

As I have seen lots of misunderstanding on how PATCH works, I aim to clarify its usage before diving into the actual solution.

Table of Contents

The problem with PUT and the need for PATCH

Consider, for example, we are creating an API to manage contacts. On the server, we have a resource that can be represented with the following JSON document:

{
"id": 1,
"name": "John Appleseed",
"work": {
"title": "Engineer",
"company": "Acme"
},
"phones": [
{
"phone": "0000000000",
"type": "mobile"
}
]
}

Let's say that John has been promoted to senior engineer and we want to keep our contact list updated. We could modify this resource using a PUT request, as shown below:

PUT /contacts/1 HTTP/1.1Host: example.orgContent-Type: application/json
{
"id": 1,
"name": "John Appleseed",
"work": {
"title": "Senior Engineer",
"company": "Acme"
},
"phones": [
{
"phone": "0000000000",
"type": "mobile"
}
]
}

With PUT, however, we have to send the full representation of the resource even when we need to modify a single field of a resource, which may not be desirable in some situations.

Let's have a look on how the PUT HTTP method is defined in the RFC 7231, one of the documents that currently define the HTTP/1.1 protocol:

4.3.4. PUT

The PUT method requests that the state of the target resource be created or replaced with the state defined by the representation enclosed in the request message payload. [...]

So, as per definition, the PUT method is meant to be used for:

  • Creating resources [1]
  • Replacing the state of a given resource

The key here is: the PUT payload must be a new representation of the resource. Hence it's not meant for performing partial modifications to resources at all. To fill this gap, the PATCH method was created and it is currently defined in the RFC 5789:

2. The PATCH Method

The PATCH method requests that a set of changes described in the request entity be applied to the resource identified by the Request-URI. The set of changes is represented in a format called a "patch document" identified by a media type. [...]

The difference between the PUT and PATCH requests is reflected in the way the server processes the request payload to modify a given resource:

  • In a PUT request, the payload is a modified version of the resource stored on the server. And the client is requesting the stored version to be replaced with the new version.
  • In a PATCH request, the request payload contains a set of instructions describing how a resource currently stored on the server should be modified to produce a new version.

Describing how the resource will be modified

The PATCH method definition, however, doesn't enforce any format for the request payload apart from mentioning that the request payload should contain a set of instructions describing how the resource will be modified and that set of instructions is identified by a media type.

Let's have a look at some formats for describing how a resource is to be PATCHed:

JSON Patch

JSON Patch is a format for expressing a sequence of operations to be applied to a JSON document. It is defined in the RFC 6902 and is identified by the application/json-patch+json media type.

The JSON Patch document represents an array of objects and each object represents a single operation to be applied to the target JSON document.

The evaluation of a JSON Patch document begins against a target JSON document and the operations are applied sequentially in the order they appear in the array. Each operation in the sequence is applied to the target document and the resulting document becomes the target of the next operation. The evaluation continues until all operations are successfully applied or until an error condition is encountered.

The operation objects must have exactly one op member, whose value indicates the operation to perform:

OperationDescription
addAdds the value at the target location; if the value exists in the given location, it's replaced
removeRemoves the value at the target location
replaceReplaces the value at the target location
moveRemoves the value at a specified location and adds it to the target location
copyCopies the value at a specified location to the target location
testTests that a value at the target location is equal to a specified value

Any other values are considered errors.

A request to modify John's job title could be:

PATCH /contacts/1 HTTP/1.1Host: example.orgContent-Type: application/json-patch+json[
{ "op": "replace", "path": "/work/title", "value": "Senior Engineer" }
]

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.

The server processing a JSON Merge Patch document determine the exact set of changes being requested by comparing the content of the provided patch against the current content of the target document:

  • If the merge patch contains members that do not appear within the target document, those members are added
  • If the target does contain the member, the value is replaced
  • null values in the merge patch indicate that existing values in the target document are to be removed
  • Other values in the target document will remain untouched

A request to modify John's job title could be:

PATCH /contacts/1 HTTP/1.1Host: example.orgContent-Type: application/merge-patch+json
{
"work": {
"title": "Senior Engineer"
}
}

JSON-P: Java API for JSON Processing

JSON-P 1.0, defined in the JSR 353 and also known as Java API for JSON Processing 1.0, brought official support for JSON processing in Java EE. JSON-P 1.1, defined in the JSR 374, introduced support for JSON Patch and JSON Merge Patch formats to Java EE.

Let's have a quick look at the API to start getting familiar with it:

TypeDescription
JsonFactory class for creating JSON processing objects
JsonPatchRepresents an implementation of JSON Patch
JsonMergePatchRepresents an implementation of JSON Merge Patch
JsonValueRepresents an immutable JSON value that can be an object, an array, a number, a string, true, false or null
JsonStructureSuper type for the two structured types in JSON: object and array

To patch using JSON Patch, we would have the following:

// Target JSON document to be patchedJsonObjecttarget = ...;
// Create JSON Patch documentJsonPatchjsonPatch = Json.createPatchBuilder()
.replace("/work/title", "Senior Engineer")
.build();
// Apply the patch to the target documentJsonValuepatched = jsonPatch.apply(target);

And to patch using JSON Merge Patch, we would have the following:

// Target JSON document to be patchedJsonObjecttarget = ...;
// Create JSON Merge Patch documentJsonMergePatchmergePatch = Json.createMergePatch(Json.createObjectBuilder()
.add("work", Json.createObjectBuilder()
.add("title", "Senior Engineer"))
.build());
// Apply the patch to the target documentJsonValuepatched = mergePatch.apply(target);

Having said that, let me highlight that JSON-P is just an API, that is, a set of interfaces. If we want to work with it, we need an implementation such as Apache Johnzon:

<dependency>
<groupId>org.apache.johnzon</groupId>
<artifactId>johnzon-core</artifactId>
<version>${johnzon.version}</version>
</dependency>

Parsing the request payload

To parse a PATCH request payload, we must take the following into account:

  • For an incoming request with the application/json-patch+json content type, the payload must be converted to an instance of JsonPatch.
  • For an incoming request with the application/merge-patch+json content type, the payload must be converted to an instance of JsonMergePatch.

Spring MVC, however, doesn't know how to create instances of JsonPatch and JsonMergePatch. So we need to provide a custom HttpMessageConverter<T> for each type. Fortunately it's pretty straightforward.

For convenience, let's extend AbstractHttpMessageConverter<T> and annotate the implementation with @Component, so Spring can pick it up:

@ComponentpublicclassJsonPatchHttpMessageConverterextendsAbstractHttpMessageConverter<JsonPatch> {
...
}

The constructor will invoke the parent's constructor indicating the supported media type for this converter:

publicJsonPatchHttpMessageConverter() {
super(MediaType.valueOf("application/json-patch+json"));
}

We indicate that our converter supports the JsonPatch class:

@Overrideprotectedbooleansupports(Class<?> clazz) {
returnJsonPatch.class.isAssignableFrom(clazz);
}

Then we implement the method that will read the HTTP request payload and convert it to a JsonPatch instance:

@OverrideprotectedJsonPatchreadInternal(Class<? extendsJsonPatch> clazz, HttpInputMessageinputMessage)
throwsHttpMessageNotReadableException {
try (JsonReaderreader = Json.createReader(inputMessage.getBody())) {
returnJson.createPatch(reader.readArray());
} catch (Exceptione) {
thrownewHttpMessageNotReadableException(e.getMessage(), inputMessage);
}
}

It's unlikely we'll need to write JsonPatch instances to the responses, but we could implement it as follows:

@OverrideprotectedvoidwriteInternal(JsonPatchjsonPatch, HttpOutputMessageoutputMessage)
throwsHttpMessageNotWritableException {
try (JsonWriterwriter = Json.createWriter(outputMessage.getBody())) {
writer.write(jsonPatch.toJsonArray());
} catch (Exceptione) {
thrownewHttpMessageNotWritableException(e.getMessage(), e);
}
}

The message converter for JsonMergePatch is pretty much the same as the converter described above (except for the types handled by the converter):

@ComponentpublicclassJsonMergePatchHttpMessageConverterextendsAbstractHttpMessageConverter<JsonMergePatch> {
publicJsonMergePatchHttpMessageConverter() {
super(MediaType.valueOf("application/merge-patch+json"));
}
@Overrideprotectedbooleansupports(Class<?> clazz) {
returnJsonMergePatch.class.isAssignableFrom(clazz);
}
@OverrideprotectedJsonMergePatchreadInternal(Class<? extendsJsonMergePatch> clazz, HttpInputMessageinputMessage)
throwsHttpMessageNotReadableException {
try (JsonReaderreader = Json.createReader(inputMessage.getBody())) {
returnJson.createMergePatch(reader.readValue());
} catch (Exceptione) {
thrownewHttpMessageNotReadableException(e.getMessage(), inputMessage);
}
}
@OverrideprotectedvoidwriteInternal(JsonMergePatchjsonMergePatch, HttpOutputMessageoutputMessage)
throwsHttpMessageNotWritableException {
try (JsonWriterwriter = Json.createWriter(outputMessage.getBody())) {
writer.write(jsonMergePatch.toJsonValue());
} catch (Exceptione) {
thrownewHttpMessageNotWritableException(e.getMessage(), e);
}
}
}

Creating the controller methods

With the HTTP message converters in place, we can receive JsonPatch and JsonMergePatch as method arguments in our controller methods, annotated with @RequestBody:

@PatchMapping(path = "/{id}", consumes = "application/json-patch+json")
publicResponseEntity<Void> updateContact(@PathVariableLongid,
@RequestBodyJsonPatchpatchDocument) {
...
}
@PatchMapping(path = "/{id}", consumes = "application/merge-patch+json")
publicResponseEntity<Void> updateContact(@PathVariableLongid,
@RequestBodyJsonMergePatchmergePatchDocument) {
...
}

Applying the patch

It is worth it to mention that both JSON Patch and JSON Merge Patch operate over JSON documents.

So, to apply the patch to a Java bean, we first need to convert the Java bean to a JSON-P type, such as JsonStructure or JsonValue. Then we apply the patch to it and convert the patched document back to a Java bean:

Patch conversions

These conversions could be handled by Jackson, which provides an extension module to work with JSON-P types. With this extension module, we can read JSON as JsonValues and write JsonValues as JSON as part of normal Jackson processing, taking advantage of the powerful data-binding features that Jackson provides:

<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr353</artifactId>
<version>${jackson.version}</version>
</dependency>

With module extension dependency on the classpath, we can configure the ObjectMapper and expose it as a Spring @Bean (so it can be picked up by String and can be injected in other Spring beans):

@BeanpublicObjectMapperobjectMapper() {
returnnewObjectMapper()
.setDefaultPropertyInclusion(Include.NON_NULL)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.findAndRegisterModules();
}

The findAndRegisterModules() method is important here: it tells Jackson to search and register the any modules found in the classpath, including the jackson-datatype-jsr353 extension module. Alternatively, we can register the module manually:

mapper.registerModule(newJSR353Module());

Once the ObjectMapper is configured, we can inject it in our Spring beans and create a method to apply the JSON Patch to a Java bean:

public <T> Tpatch(JsonPatchpatch, TtargetBean, Class<T> beanClass) {
// Convert the Java bean to a JSON documentJsonStructuretarget = mapper.convertValue(targetBean, JsonStructure.class);
// Apply the JSON Patch to the JSON documentJsonValuepatched = patch.apply(target);
// Convert the JSON document to a Java bean and return itreturnmapper.convertValue(patched, beanClass);
}

And here's the method to patch using JSON Merge Patch:

public <T> TmergePatch(JsonMergePatchmergePatch, TtargetBean, Class<T> beanClass) {
// Convert the Java bean to a JSON documentJsonValuetarget = mapper.convertValue(targetBean, JsonValue.class);
// Apply the JSON Merge Patch to the JSON documentJsonValuepatched = mergePatch.apply(target);
// Convert the JSON document to a Java bean and return itreturnmapper.convertValue(patched, beanClass);
}

With this in place, the controller method implementation for JSON Patch could be like:

@PatchMapping(path = "/{id}", consumes = "application/json-patch+json")
publicResponseEntity<Void> updateContact(@PathVariableLongid,
@RequestBodyJsonPatchpatchDocument) {
// Find the model that will be patchedContactcontact = contactService.findContact(id).orElseThrow(ResourceNotFoundException::new);
// Apply the patchContactcontactPatched = patch(patchDocument, contact, Contact.class);
// Persist the changescontactService.updateContact(contactPatched);
// Return 204 to indicate the request has succeededreturnResponseEntity.noContent().build();
}

And the implementation is quite similar for JSON Merge Patch, except for the media type and for the types handled by the method:

@PatchMapping(path = "/{id}", consumes = "application/merge-patch+json")
publicResponseEntity<Void> updateContact(@PathVariableLongid,
@RequestBodyJsonMergePatchmergePatchDocument) {
// Find the model that will be patchedContactcontact = contactService.findContact(id).orElseThrow(ResourceNotFoundException::new);
// Apply the patchContactcontactPatched = mergePatch(mergePatchDocument, contact, Contact.class);
// Persist the changescontactService.updateContact(contactPatched);
// Return 204 to indicate the request has succeededreturnResponseEntity.noContent().build();
}

Validating the patch

Once the patch has been applied and before persisting the changes, we must ensure that the patch didn't lead the resource to an invalid state. We could use Bean Validation annotations to define constraints and then ensure that the state of the model is valid.

publicclassContact {
@NotBlankprivateStringname;
...
}

To perform the validation, we could inject Validator in our class and invoke the validate() method. If any constraint has been violated, it will return a set of ConstraintViolation<T> and then we can throw a ConstraintViolationException. So the method to apply the patch could be updated to handle the validation, as shown below:

public <T> Tpatch(JsonPatchpatch, TtargetBean, Class<T> beanClass) {
// Convert the Java bean to a JSON documentJsonStructuretarget = mapper.convertValue(targetBean, JsonStructure.class);
// Apply the JSON Patch to the JSON documentJsonValuepatched = applyPatch(patch, target);
// Convert the JSON document to a Java beanTbeanPatched = mapper.convertValue(patched, beanClass);
// Validate the Java bean and throw an excetion if any constraint has been violatedSet<ConstraintViolation<T>> violations = validator.validate(beanPatched);
if (!violations.isEmpty()) {
thrownewConstraintViolationException(violations);
}
// Return the bean that has been patchedreturnbeanPatched;
}

Alternatively, we could simply annotate the method with @Valid and Bean Validation will take care of performing the the validation on the returned value (the Spring bean may need to be annotated with @Validated to trigger the validation):

@Validpublic <T> Tpatch(JsonPatchpatch, TtargetBean, Class<T> beanClass) {
// Convert the Java bean to a JSON documentJsonStructuretarget = mapper.convertValue(targetBean, JsonStructure.class);
// Apply the JSON Patch to the JSON documentJsonValuepatched = applyPatch(patch, target);
// Convert the JSON document to a Java bean and return itreturnmapper.convertValue(patched, beanClass);
}

Bonus: Decoupling the domain model from the API model

The models that represent the domain of our application and the models that represent the data handled by our API are (or at least should be) different concerns and should be decoupled from each other. We don't want to break our API clients when we add, remove or rename a field from the application domain model.[2]

While our service layer operates over the domain/persistence models, our API controllers should operate over a different set of models. As our domain/persistence models evolve to support new business requirements, for example, we may want to create new versions of the API models to support these changes. We also may want to deprecate the old versions of our API as new versions are released. And it's perfectly possible to achieve when the things are decoupled.

To minimize the boilerplate code of converting the domain model to the API model (and vice versa), we could rely on frameworks such as MapStruct. And we also could consider using Lombok to generate getters, setters, equals(), hashcode() and toString() methods for us.

By decoupling the API model from domain model, we also can ensure that we expose only the fields that can be modified. For example, we don't want to allow the client to modify the id field of our domain model. So our API model shouldn't contain the id field (and any attempt to modify it may cause an error or may be ignored).

In this example, the domain model class is called Contact and the model class that represents a resource is called ContactResourceInput. To convert between these two models with MapStruct, we could define a mapper interface and MapStruct will generate an implementation for it:

@Mapper(componentModel = "spring")
publicinterfaceContactMapper {
ContactResourceInputasContactResourceInput(Contactcontact);
voidupdate(ContactResourceInputcontactResource, @MappingTargetContactcontact);
...
}

The ContactMapper implementation will be exposed as a Spring @Component, so it can be injected in other Spring beans. Let me highlight that MapStruct doesn't use reflections. Instead, it creates an actual implementation for the mapper interface and we can even check the code if we want to.

Once the ContactMapper is injected in our controller, we can use it to handle the model conversion. Here's what the controller method for handling PATCH requests with JSON Patch could be like:

@PatchMapping(path = "/{id}", consumes = "application/json-patch+json")
publicResponseEntity<Void> updateContact(@PathVariableLongid,
@RequestBodyJsonPatchpatchDocument) {
// Find the domain model that will be patchedContactcontact = contactService.findContact(id).orElseThrow(ResourceNotFoundException::new);
// Map the domain model to an API resource modelContactResourceInputcontactResource = contactMapper.asContactResourceInput(contact);
// Apply the patch to the API resource modelContactResourceInputcontactResourcePatched = patch(patchDocument, contactResource, ContactResourceInput.class);
// Update the domain model with the details from the API resource modelcontactMapper.update(contactResourcePatched, contact);
// Persist the changescontactService.updateContact(contact);
// Return 204 to indicate the request has succeededreturnResponseEntity.noContent().build();
}

And, for comparision purposes, here's a controller method for handling PUT requests:

@PutMapping(path = "/{id}", consumes = "application/json")
publicResponseEntity<Void> updateContact(@PathVariableLongid,
@RequestBody@ValidContactResourceInputcontactResource) {
// Find the domain model that will be updatedContactcontact = contactService.findContact(id).orElseThrow(ResourceNotFoundException::new);
// Update the domain model with the details from the API resource modelcontactMapper.update(contactResource, contact);
// Persist the changescontactService.updateContact(contact);
// Return 204 to indicate the request has succeededreturnResponseEntity.noContent().build();
}

References


Footnotes

[1] You may not want to support PUT for creating resources if you rely on the server to generate identifiers for your resources. See my answer on Stack Overflow for details on this.

[2] I also have described the benefits of this approach in this answer on Stack Overflow.

Releases

Packages

Used by

Contributors

Languages