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 for PATCH
  • 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 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:

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 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:

                          @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:

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):

                          @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

robertsgoder1939.blogspot.com

Source: https://cassiomolin.com/2019/06/10/using-http-patch-in-spring/

0 Response to "Java Spring Put Request Example"

Post a Comment

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel