Utilizzo della patch JSON nelle API REST di primavera

1. Introduzione

Tra i vari metodi HTTP disponibili, il metodo HTTP PATCH gioca un ruolo unico. Ci consente di applicare aggiornamenti parziali alle risorse HTTP.

In questo tutorial, vedremo come utilizzare il metodo HTTP PATCH insieme al formato del documento JSON Patch per applicare aggiornamenti parziali alle nostre risorse RESTful.

2. Il caso d'uso

Cominciamo considerando un esempio di risorsa del cliente HTTP rappresentata dal documento JSON:

{ "id":"1", "telephone":"001-555-1234", "favorites":["Milk","Eggs"], "communicationPreferences": {"post":true, "email":true} }

Supponiamo che il numero di telefono di questo clienteè cambiato e il cliente ha aggiunto un nuovo articolo al proprio elenco di prodotti preferiti. Ciò significa che dobbiamo aggiornare solo i campi telefono e preferiti del cliente .

Come lo faremmo?

Per prima cosa viene in mente il popolare metodo HTTP PUT. Tuttavia, poiché il PUT sostituisce completamente una risorsa, non è un metodo adatto per applicare gli aggiornamenti parziali in modo elegante. Inoltre, i client devono eseguire un GET prima che gli aggiornamenti vengano applicati e salvati.

È qui che il metodo HTTP PATCH torna utile.

Comprendiamo il metodo HTTP PATCH e i formati JSON Patch.

3. Il metodo PATCH HTTP e il formato patch JSON

Il metodo HTTP PATCH offre un bel modo per applicare aggiornamenti parziali alle risorse. Di conseguenza, i clienti devono inviare solo le differenze nelle loro richieste.

Diamo un'occhiata a un semplice esempio di una richiesta PATCH HTTP:

PATCH /customers/1234 HTTP/1.1 Host: www.example.com Content-Type: application/example If-Match: "e0023aa4e" Content-Length: 100 [description of changes]

Il corpo della richiesta PATCH HTTP descrive come modificare la risorsa di destinazione per produrre una nuova versione. Inoltre, il formato utilizzato per rappresentare la [descrizione delle modifiche] varia a seconda del tipo di risorsa. Per i tipi di risorse JSON, il formato utilizzato per descrivere le modifiche è JSON Patch.

In poche parole, il formato JSON Patch utilizza una "serie di operazioni" per descrivere come la risorsa di destinazione dovrebbe essere modificata. Un documento JSON Patch è un array di oggetti JSON. Ogni oggetto nell'array rappresenta esattamente un'operazione JSON Patch.

Esaminiamo ora le operazioni della patch JSON insieme ad alcuni esempi.

4. Operazioni di patch JSON

Un'operazione JSON patch è rappresentato da una singola op oggetto.

Ad esempio, qui stiamo definendo un'operazione di patch JSON per aggiornare il numero di telefono del cliente:

{ "op":"replace", "path":"/telephone", "value":"001-555-5678" }

Ogni operazione deve avere un membro del percorso . Inoltre, alcuni oggetti operazione devono contenere anche un membro from . Il valore del percorso e dei membri è un puntatore JSON. Si riferisce a una posizione all'interno del documento di destinazione. Questa posizione può puntare a una chiave specifica oa un elemento della matrice nell'oggetto di destinazione.

Vediamo ora brevemente le operazioni di patch JSON disponibili.

4.1. L' operazione di aggiunta

Usiamo l' add operazione per aggiungere un nuovo membro a un oggetto. Inoltre, possiamo usarlo per aggiornare un membro esistente e per inserire un nuovo valore nell'array all'indice specificato.

Ad esempio, aggiungiamo "Pane" all'elenco dei preferiti del cliente all'indice 0:

{ "op":"add", "path":"/favorites/0", "value":"Bread" }

I dettagli del cliente modificati dopo l' operazione di aggiunta sarebbero:

{ "id":"1", "telephone":"001-555-1234", "favorites":["Bread","Milk","Eggs"], "communicationPreferences": {"post":true, "email":true} }

4.2. L' operazione di rimozione

La rimozione operazione rimuove un valore al percorso di destinazione. Inoltre, può rimuovere un elemento da un array all'indice specificato.

Ad esempio, rimuoviamo le preferenze di comunicazione per il nostro cliente:

{ "op":"remove", "path":"/communicationPreferences" }

I dettagli del cliente modificati dopo l' operazione di rimozione sarebbero:

{ "id":"1", "telephone":"001-555-1234", "favorites":["Bread","Milk","Eggs"], "communicationPreferences":null }

4.3. L' operazione di sostituzione

L' operazione di sostituzione aggiorna il valore nella posizione di destinazione con un nuovo valore.

Ad esempio, aggiorniamo il numero di telefono del nostro cliente:

{ "op":"replace", "path":"/telephone", "value":"001-555-5678" }

I dettagli del cliente modificati dopo l' operazione di sostituzione sarebbero:

{ "id":"1", "telephone":"001-555-5678", "favorites":["Bread","Milk","Eggs"], "communicationPreferences":null }

4.4. L' operazione di spostamento

L' operazione di spostamento rimuove il valore nella posizione specificata e lo aggiunge alla posizione di destinazione.

Ad esempio, spostiamo "Pane" dalla parte superiore dell'elenco dei preferiti del cliente alla fine dell'elenco:

{ "op":"move", "from":"/favorites/0", "path":"/favorites/-" }

I dettagli del cliente modificati dopo l' operazione di spostamento sarebbero:

{ "id":"1", "telephone":"001-555-5678", "favorites":["Milk","Eggs","Bread"], "communicationPreferences":null } 

Il / preferiti / 0 e / preferiti / - nell'esempio sopra sono JSON puntatori alle indici di inizio e fine della favoriti matrice.

4.5. L' operazione di copia

The copy operation copies the value at the specified location to the target location.

For example, let's duplicate “Milk” in the favorites list:

{ "op":"copy", "from":"/favorites/0", "path":"/favorites/-" }

The modified customer details after the copy operation would be:

{ "id":"1", "telephone":"001-555-5678", "favorites":["Milk","Eggs","Bread","Milk"], "communicationPreferences":null }

4.6. The test Operation

The test operation tests that the value at the “path” is equal to the “value”. Because the PATCH operation is atomic, the PATCH should be discarded if any of its operations fail. The test operation can be used to validate that the preconditions and post-conditions have been met.

For instance, let's test that the update to the customer's telephone field has been successful:

{ "op":"test", "path":"/telephone", "value":"001-555-5678" } 

Let's now see how we can apply the above concepts to our example.

5. HTTP PATCH Request Using the JSON Patch Format

We'll revisit our Customer use case.

Here is the HTTP PATCH request to perform a partial update to the customer's telephone and favorites list using the JSON Patch format:

curl -i -X PATCH //localhost:8080/customers/1 -H "Content-Type: application/json-patch+json" -d '[ {"op":"replace","path":"/telephone","value":"+1-555-56"}, {"op":"add","path":"/favorites/0","value":"Bread"} ]' 

Most importantly, the Content-Type for JSON Patch requests is application/json-patch+json. Also, the request body is an array of JSON Patch operation objects:

[ {"op":"replace","path":"/telephone","value":"+1-555-56"}, {"op":"add","path":"/favorites/0","value":"Bread"} ]

How would we process such a request on the server-side?

One way is to write a custom framework that evaluates the operations sequentially and applies them to the target resource as an atomic unit. Clearly, this approach sounds complicated. Also, it can lead to a non-standardized way of consuming patch documents.

Fortunately, we do not have to hand-craft the processing of JSON Patch requests.

The Java API for JSON Processing 1.0, or JSON-P 1.0, defined originally in JSR 353, introduced support for the JSON Patch in JSR 374. The JSON-P API provides the JsonPatch type to represent the JSON Patch implementation.

However, JSON-P is only an API. To work with the JSON-P API, we need to use a library that implements it. We'll use one such library called json-patch for the examples in this article.

Let's now look at how we can build a REST service that consumes HTTP PATCH requests using the JSON Patch format described above.

6. Implementing JSON Patch in a Spring Boot Application

6.1. Dependencies

The latest version of json-patch can be found from the Maven Central repository.

To begin with, let's add the dependencies to the pom.xml:

 com.github.java-json-tools json-patch 1.12 

Now, let's define a schema class to represent the Customer JSON document :

public class Customer { private String id; private String telephone; private List favorites; private Map communicationPreferences; // standard getters and setters }

Next, we'll look at our controller method.

6.2. The REST Controller Method

Then, we can implement HTTP PATCH for our customer use case:

@PatchMapping(path = "/{id}", consumes = "application/json-patch+json") public ResponseEntity updateCustomer(@PathVariable String id, @RequestBody JsonPatch patch) { try { Customer customer = customerService.findCustomer(id).orElseThrow(CustomerNotFoundException::new); Customer customerPatched = applyPatchToCustomer(patch, customer); customerService.updateCustomer(customerPatched); return ResponseEntity.ok(customerPatched); } catch (JsonPatchException | JsonProcessingException e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } catch (CustomerNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } } 

Let's now understand what is going on in this method:

  • To begin with, we use the @PatchMapping annotation to mark the method as a PATCH handler method
  • When a patch request with the application/json-patch+json “Content-Type” arrives, Spring Boot uses the default MappingJackson2HttpMessageConverter to convert the request payload to a JsonPatch instance. As a result, our controller method will receive the request body as a JsonPatch instance

Within the method:

  1. First, we call the customerService.findCustomer(id) method to find the customer record
  2. Subsequently, if the customer record is found, we invoke the applyPatchToCustomer(patch, customer) method. This applies the JsonPatch to the customer (more on this later)
  3. We then invoke the customerService.updateCustomer(customerPatched) to save the customer record
  4. Finally, we return a 200 OK response to the client with the patched Customer details in the response

Most importantly, the real magic happens in the applyPatchToCustomer(patch, customer) method:

private Customer applyPatchToCustomer( JsonPatch patch, Customer targetCustomer) throws JsonPatchException, JsonProcessingException { JsonNode patched = patch.apply(objectMapper.convertValue(targetCustomer, JsonNode.class)); return objectMapper.treeToValue(patched, Customer.class); } 
  1. To begin with, we have our JsonPatch instance that holds the list of operations to be applied to the target Customer
  2. We then convert the target Customer into an instance of com.fasterxml.jackson.databind.JsonNode and pass it to the JsonPatch.apply method to apply the patch. Behind the scenes, the JsonPatch.apply deals with applying the operations to the target. The result of the patch is also a com.fasterxml.jackson.databind.JsonNode instance
  3. We then call the objectMapper.treeToValue method, which binds the data in the patched com.fasterxml.jackson.databind.JsonNode to the Customer type. This is our patched Customer instance
  4. Finally, we return the patched Customer instance

Let's now run some tests against our API.

6.3. Testing

To begin with, let's create a customer using a POST request to our API:

curl -i -X POST //localhost:8080/customers -H "Content-Type: application/json" -d '{"telephone":"+1-555-12","favorites":["Milk","Eggs"],"communicationPreferences":{"post":true,"email":true}}' 

We receive a 201 Created response:

HTTP/1.1 201 Location: //localhost:8080/customers/1 

The Location response header is set to the location of the new resource. It indicates that the id of the new Customer is 1.

Next, let's request a partial update to this customer using a PATCH request:

curl -i -X PATCH //localhost:8080/customers/1 -H "Content-Type: application/json-patch+json" -d '[ {"op":"replace","path":"/telephone","value":"+1-555-56"}, {"op":"add","path":"/favorites/0","value": "Bread"} ]'

We receive a 200OK response with the patched customer details:

HTTP/1.1 200 Content-Type: application/json Transfer-Encoding: chunked Date: Fri, 14 Feb 2020 21:23:14 GMT {"id":"1","telephone":"+1-555-56","favorites":["Bread","Milk","Eggs"],"communicationPreferences":{"post":true,"email":true}}

7. Conclusion

In this article, we looked at how to implement JSON Patch in Spring REST APIs.

To begin with, we looked at the HTTP PATCH method and its ability to perform partial updates.

We then looked into what is JSON Patch and understood the various JSON Patch operations.

Infine, abbiamo discusso come gestire una richiesta HTTP PATCH in un'applicazione Spring Boot utilizzando la libreria json-patch.

Come sempre, il codice sorgente per gli esempi utilizzati in questo articolo è disponibile su GitHub.