Java Spring Put Request Example
This post demonstrates an approach to support HTTP PATCH
with JSON Patch and JSON Merge Patch for performing partial modifications to resources in Spring. As I have seen lots of misunderstanding on how PATCH
works, I aim to clarify its usage before diving into the actual solution.
This post is heavy on code examples and the full source code is available on GitHub. I also have put together a Postman collection so you can play around with the API.
Table of contents
- The problem with
PUT
and the need forPATCH
- Describing how the resource will be modified
- JSON Patch
- JSON Merge Patch
- JSON-P: Java API for JSON Processing
- Parsing the request payload
- Creating the controller methods
- Applying the patch
- Validating the patch
- Bonus: Decoupling the domain model from the API model
- References
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.1 Host : example.org Content-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 PATCH
ed:
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:
Operation | Description |
---|---|
add | Adds the value at the target location; if the value exists in the given location, it's replaced |
remove | Removes the value at the target location |
replace | Replaces the value at the target location |
move | Removes the value at a specified location and adds it to the target location |
copy | Copies the value at a specified location to the target location |
test | Tests 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.1 Host : example.org Content-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.1 Host : example.org Content-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:
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 |
To patch using JSON Patch, we would have the following:
// Target JSON document to be patched JsonObject target = ...; // Create JSON Patch document JsonPatch jsonPatch = Json . createPatchBuilder () . replace ( "/work/title" , "Senior Engineer" ) . build (); // Apply the patch to the target document JsonValue patched = jsonPatch . apply ( target );
And to patch using JSON Merge Patch, we would have the following:
// Target JSON document to be patched JsonObject target = ...; // Create JSON Merge Patch document JsonMergePatch mergePatch = Json . createMergePatch ( Json . createObjectBuilder () . add ( "work" , Json . createObjectBuilder () . add ( "title" , "Senior Engineer" )) . build ()); // Apply the patch to the target document JsonValue patched = 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 ofJsonPatch
. - For an incoming request with the
application/merge-patch+json
content type, the payload must be converted to an instance ofJsonMergePatch
.
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:
@Component public class JsonPatchHttpMessageConverter extends AbstractHttpMessageConverter < JsonPatch > { ... }
The constructor will invoke the parent's constructor indicating the supported media type for this converter:
public JsonPatchHttpMessageConverter () { super ( MediaType . valueOf ( "application/json-patch+json" )); }
We indicate that our converter supports the JsonPatch
class:
@Override protected boolean supports ( Class <?> clazz ) { return JsonPatch . class . isAssignableFrom ( clazz ); }
Then we implement the method that will read the HTTP request payload and convert it to a JsonPatch
instance:
@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 ); } }
It's unlikely we'll need to write JsonPatch
instances to the responses, but we could implement it as follows:
@Override protected void writeInternal ( JsonPatch jsonPatch , HttpOutputMessage outputMessage ) throws HttpMessageNotWritableException { try ( JsonWriter writer = Json . createWriter ( outputMessage . getBody ())) { writer . write ( jsonPatch . toJsonArray ()); } catch ( Exception e ) { throw new HttpMessageNotWritableException ( 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):
@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 > clazz , HttpInputMessage inputMessage ) throws HttpMessageNotReadableException { try ( JsonReader reader = Json . createReader ( inputMessage . getBody ())) { return Json . createMergePatch ( reader . readValue ()); } catch ( Exception e ) { throw new HttpMessageNotReadableException ( e . getMessage (), inputMessage ); } } @Override protected void writeInternal ( JsonMergePatch jsonMergePatch , HttpOutputMessage outputMessage ) throws HttpMessageNotWritableException { try ( JsonWriter writer = Json . createWriter ( outputMessage . getBody ())) { writer . write ( jsonMergePatch . toJsonValue ()); } catch ( Exception e ) { throw new HttpMessageNotWritableException ( 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" ) public ResponseEntity < Void > updateContact ( @PathVariable Long id , @RequestBody JsonPatch patchDocument ) { ... }
@PatchMapping ( path = "/{id}" , consumes = "application/merge-patch+json" ) public ResponseEntity < Void > updateContact ( @PathVariable Long id , @RequestBody JsonMergePatch mergePatchDocument ) { ... }
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:
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 JsonValue
s and write JsonValue
s 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):
@Bean public ObjectMapper objectMapper () { return new ObjectMapper () . 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 ( new JSR353Module ());
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 > T patch ( JsonPatch patch , T targetBean , Class < T > beanClass ) { // Convert the Java bean to a JSON document JsonStructure target = mapper . 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 mapper . convertValue ( patched , beanClass ); }
And here's the method to patch using JSON Merge Patch:
public < T > T mergePatch ( JsonMergePatch mergePatch , T targetBean , Class < T > beanClass ) { // Convert the Java bean to a JSON document JsonValue target = mapper . convertValue ( targetBean , JsonValue . class ); // Apply the JSON Merge Patch to the JSON document JsonValue patched = mergePatch . apply ( target ); // Convert the JSON document to a Java bean and return it return mapper . 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" ) public ResponseEntity < Void > updateContact ( @PathVariable Long id , @RequestBody JsonPatch patchDocument ) { // Find the model that will be patched Contact contact = contactService . findContact ( id ). orElseThrow ( ResourceNotFoundException: : new ); // Apply the patch Contact contactPatched = patch ( patchDocument , contact , Contact . class ); // Persist the changes contactService . updateContact ( contactPatched ); // Return 204 to indicate the request has succeeded return ResponseEntity . 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" ) public ResponseEntity < Void > updateContact ( @PathVariable Long id , @RequestBody JsonMergePatch mergePatchDocument ) { // Find the model that will be patched Contact contact = contactService . findContact ( id ). orElseThrow ( ResourceNotFoundException: : new ); // Apply the patch Contact contactPatched = mergePatch ( mergePatchDocument , contact , Contact . class ); // Persist the changes contactService . updateContact ( contactPatched ); // Return 204 to indicate the request has succeeded return ResponseEntity . 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.
public class Contact { @NotBlank private String name ; ... }
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 > T patch ( JsonPatch patch , T targetBean , Class < T > beanClass ) { // Convert the Java bean to a JSON document JsonStructure target = mapper . convertValue ( targetBean , JsonStructure . class ); // Apply the JSON Patch to the JSON document JsonValue patched = applyPatch ( patch , target ); // Convert the JSON document to a Java bean T beanPatched = mapper . convertValue ( patched , beanClass ); // Validate the Java bean and throw an excetion if any constraint has been violated Set < ConstraintViolation < T >> violations = validator . validate ( beanPatched ); if (! violations . isEmpty ()) { throw new ConstraintViolationException ( violations ); } // Return the bean that has been patched return beanPatched ; }
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):
@Valid public < T > T patch ( JsonPatch patch , T targetBean , Class < T > beanClass ) { // Convert the Java bean to a JSON document JsonStructure target = mapper . convertValue ( targetBean , JsonStructure . class ); // Apply the JSON Patch to the JSON document JsonValue patched = applyPatch ( patch , target ); // Convert the JSON document to a Java bean and return it return mapper . 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" ) public interface ContactMapper { ContactResourceInput asContactResourceInput ( Contact contact ); void update ( ContactResourceInput contactResource , @MappingTarget Contact contact ); ... }
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" ) public ResponseEntity < Void > updateContact ( @PathVariable Long id , @RequestBody JsonPatch patchDocument ) { // Find the domain model that will be patched Contact contact = contactService . findContact ( id ). orElseThrow ( ResourceNotFoundException: : new ); // Map the domain model to an API resource model ContactResourceInput contactResource = contactMapper . asContactResourceInput ( contact ); // Apply the patch to the API resource model ContactResourceInput contactResourcePatched = patch ( patchDocument , contactResource , ContactResourceInput . class ); // Update the domain model with the details from the API resource model contactMapper . update ( contactResourcePatched , contact ); // Persist the changes contactService . updateContact ( contact ); // Return 204 to indicate the request has succeeded return ResponseEntity . noContent (). build (); }
And, for comparision purposes, here's a controller method for handling PUT
requests:
@PutMapping ( path = "/{id}" , consumes = "application/json" ) public ResponseEntity < Void > updateContact ( @PathVariable Long id , @RequestBody @Valid ContactResourceInput contactResource ) { // Find the domain model that will be updated Contact contact = contactService . findContact ( id ). orElseThrow ( ResourceNotFoundException: : new ); // Update the domain model with the details from the API resource model contactMapper . update ( contactResource , contact ); // Persist the changes contactService . updateContact ( contact ); // Return 204 to indicate the request has succeeded return ResponseEntity . noContent (). build (); }
References
- RFC 7231: Semantics and content for the HTTP/1.1 protocol
- RFC 5789: HTTP
PATCH
method - RFC 6902: JSON Patch
- RFC 7396: JSON Merge Patch
-
javax.json
: Java API for JSON processing
Footnotes
Source: https://cassiomolin.com/2019/06/10/using-http-patch-in-spring/
0 Response to "Java Spring Put Request Example"
Post a Comment