Utilisation du correctif JSON dans les API REST Spring

1. Introduction

Parmi les différentes méthodes HTTP disponibles, la méthode HTTP PATCH joue un rôle unique. Cela nous permet d'appliquer des mises à jour partielles aux ressources HTTP.

Dans ce didacticiel, nous verrons comment utiliser la méthode HTTP PATCH avec le format de document JSON Patch pour appliquer des mises à jour partielles à nos ressources RESTful.

2. Le cas d'utilisation

Commençons par considérer un exemple de ressource Client HTTP représentée par le document JSON:

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

Supposons que le numéro de téléphone de ce clienta changé et que le client a ajouté un nouvel article à sa liste de produits préférés. Cela signifie que nous devons mettre à jour uniquement les champs téléphone et favoris du client .

Comment ferions-nous cela?

La méthode HTTP PUT populaire vient à l'esprit en premier. Cependant, comme le PUT remplace entièrement une ressource, ce n'est pas une méthode appropriée pour appliquer des mises à jour partielles avec élégance. De plus, les clients doivent effectuer un GET avant que les mises à jour ne soient appliquées et enregistrées.

C'est là que la méthode HTTP PATCH est utile.

Comprenons la méthode HTTP PATCH et les formats JSON Patch.

3. La méthode HTTP PATCH et le format de patch JSON

La méthode HTTP PATCH offre un bon moyen d'appliquer des mises à jour partielles aux ressources. En conséquence, les clients doivent envoyer uniquement les différences dans leurs demandes.

Regardons un exemple simple de requête HTTP PATCH:

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

Le corps de la requête HTTP PATCH décrit comment la ressource cible doit être modifiée pour produire une nouvelle version. De plus, le format utilisé pour représenter la [description des modifications] varie en fonction du type de ressource. Pour les types de ressources JSON, le format utilisé pour décrire les modifications est JSON Patch.

En termes simples, le format JSON Patch utilise une «série d'opérations» pour décrire comment la ressource cible doit être modifiée. Un document JSON Patch est un tableau d'objets JSON. Chaque objet du tableau représente exactement une opération JSON Patch.

Examinons maintenant les opérations JSON Patch avec quelques exemples.

4. Opérations de patch JSON

Une opération JSON Patch est représentée par un seul objet op .

Par exemple, nous définissons ici une opération de patch JSON pour mettre à jour le numéro de téléphone du client:

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

Chaque opération doit avoir un membre de chemin . En outre, certains objets d'opération doivent également contenir un membre from . La valeur du chemin et des membres est un pointeur JSON. Il fait référence à un emplacement dans le document cible. Cet emplacement peut pointer vers une clé spécifique ou un élément de tableau dans l'objet cible.

Examinons maintenant brièvement les opérations de patch JSON disponibles.

4.1. L' opération d' ajout

Nous utilisons l' add opération pour ajouter un nouveau membre à un objet. Nous pouvons également l'utiliser pour mettre à jour un membre existant et insérer une nouvelle valeur dans le tableau à l'index spécifié.

Par exemple, ajoutons "Pain" à la liste des favoris du client à l'index 0:

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

Les détails du client modifiés après l' opération d' ajout seraient:

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

4.2. L' opération de suppression

L' opération de suppression supprime une valeur à l'emplacement cible. En outre, il peut supprimer un élément d'un tableau à l'index spécifié.

Par exemple, supprimons les préférences de communication pour notre client:

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

Les détails du client modifiés après l' opération de suppression seraient:

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

4.3. L' opération de remplacement

L' opération de remplacement met à jour la valeur à l'emplacement cible avec une nouvelle valeur.

À titre d'exemple, mettons à jour le numéro de téléphone de notre client:

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

Les détails du client modifiés après l' opération de remplacement seraient:

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

4.4. L' opération de déplacement

L' opération de déplacement supprime la valeur à l'emplacement spécifié et l'ajoute à l'emplacement cible.

Par exemple, déplaçons "Pain" du haut de la liste des favoris du client vers le bas de la liste:

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

Les détails du client modifiés après l' opération de déplacement seraient:

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

Les / favorites / 0 et / favorites / - dans l'exemple ci-dessus sont des pointeurs JSON vers les index de début et de fin du tableau des favoris .

4.5. L' opération de copie

L' opération de copie copie la valeur de l'emplacement spécifié vers l'emplacement cible.

Par exemple, dupliquons "Milk" dans la liste des favoris :

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

Les détails du client modifiés après l' opération de copie seraient:

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

4.6. L' opération de test

L' opération de test teste que la valeur du «chemin» est égale à la «valeur». Étant donné que l'opération PATCH est atomique, le PATCH doit être ignoré si l'une de ses opérations échoue. L' opération de test peut être utilisée pour valider que les conditions préalables et les post-conditions ont été remplies.

Par exemple, testons que la mise à jour du champ téléphone du client a réussi:

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

Voyons maintenant comment nous pouvons appliquer les concepts ci-dessus à notre exemple.

5. Requête HTTP PATCH utilisant le format de patch JSON

Nous revisiterons notre cas d'utilisation client .

Voici la requête HTTP PATCH pour effectuer une mise à jour partielle du téléphone et de la liste des favoris du client en utilisant le format JSON Patch:

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"} ]' 

Plus important encore, le type de contenu pour les demandes de patch JSON est application / json-patch + json . En outre, le corps de la requête est un tableau d'objets d'opération JSON Patch:

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

Comment traiterions-nous une telle demande côté serveur?

Une méthode consiste à écrire un cadre personnalisé qui évalue les opérations de manière séquentielle et les applique à la ressource cible en tant qu'unité atomique. De toute évidence, cette approche semble compliquée. En outre, cela peut conduire à une manière non standardisée de consommer les documents de patch.

Heureusement, nous n'avons pas à fabriquer à la main le traitement des demandes de patch JSON.

L'API Java pour JSON Processing 1.0, ou JSON-P 1.0, définie à l'origine dans JSR 353, a introduit la prise en charge du correctif JSON dans JSR 374. L'API JSON-P fournit le type JsonPatch pour représenter l'implémentation du correctif JSON.

Cependant, JSON-P n'est qu'une API. Pour travailler avec l'API JSON-P, nous devons utiliser une bibliothèque qui l'implémente. Nous utiliserons une de ces bibliothèques appelée json-patch pour les exemples de cet 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.

Enfin, nous avons expliqué comment gérer une requête HTTP PATCH dans une application Spring Boot à l'aide de la bibliothèque json-patch.

Comme toujours, le code source des exemples utilisés dans cet article est disponible à l'adresse over sur GitHub.