Patch http verb (Spring Boot)

Introduction

It covers HTTP PATCH operations with JSON Patch and JSON Merge Patch, in the context of Spring Boot.


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.

JSON Patch is a format for expressing a sequence of operations to be applied to a JSON document.


Remark: JsonPatch validations are a nightmare, so the workaround with Optional should fit most use cases. See:

https://sites.google.com/site/pawneecity/sprint-boot/json-serialization-spring-boot#h.nl9kmojr2hx5


References

Using HTTP PATCH in Spring (10 June 2019, accessed 2024-apr-27)

https://www.student.unsw.edu.au/how-do-i-cite-electronic-sources

JSON Merge Patch (October 2014, accessed  2024-apr-27)

https://datatracker.ietf.org/doc/html/rfc7396

What is JSON Patch?

http://jsonpatch.com/

Using JSON Patch in Spring REST APIs

https://www.baeldung.com/spring-rest-json-patch

JSON Patch (RFC 6902)

https://datatracker.ietf.org/doc/html/rfc6902

JSON Pointer (RFC 6901)

https://datatracker.ietf.org/doc/html/rfc6901


JSON-P: Java API for JSON Processing

JSON-P 1.0 (aka Java API for JSON Processing 1.0) defined in JSR 253 brought support for JSON processing in Java EE.

JSON-P 1.1 defined in JSR 374 introduced support for JSON Patch and JSON Merge Patch formats to Java EE.



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


JSON-P is an API (set of interfaces). For working with it, an implementation is needed, eg: Apache Johnzon

<dependency>

    <groupId>org.apache.johnzon</groupId>

    <artifactId>johnzon-core</artifactId>

    <version>${johnzon.version}</version>

</dependency>


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.

JSON Pointer & Operations

JSON Pointer (used by JSON Patch)

JSON Pointer (IETF RFC 6901) defines a string format for identifying a specific value within a JSON document. It is used by all operations in JSON Patch to specify the part of the document to operate on. 

"" points to the root of the document

/ points to a key of "" on the root

In the following JSON:

{

  "biscuits": [

    { "name": "Digestive" },

    { "name": "Choco Leibniz" }

  ]

}

/biscuits would point to the array of biscuits and

/biscuits/1/name would point to "Choco Leibniz" 

If you need to refer to a key with ~ or / in its name, you must escape the characters with ~0 and ~1 respectively. For example, to get "baz" from { "foo/bar~": "baz" } you’d use the pointer /foo~1bar~0. 

Finally, if you need to refer to the end of an array you can use - instead of an index. For example, to refer to the end of the array of biscuits above you would use /biscuits/-. This is useful when you need to insert a value at the end of an array. 


Operations (used by JSON Patch)

Add

{ "op": "add", "path": "/biscuits/1", "value": { "name": "Ginger Nut" } }

Adds a value to an object or inserts it into an array. In the case of an array, the value is inserted before the given index. The - character can be used instead of an index to insert at the end of an array.

Remove

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

Removes a value from an object or array.

{ "op": "remove", "path": "/biscuits/0" }

Removes the first element of the array at biscuits (or just removes the “0” key if biscuits is an object)

Replace

{ "op": "replace", "path": "/biscuits/0/name", "value": "Chocolate Digestive" }

Replaces a value. Equivalent to a “remove” followed by an “add”.

Copy

{ "op": "copy", "from": "/biscuits/0", "path": "/best_biscuit" }

Copies a value from one location to another within the JSON document. Both from and path are JSON Pointers.

Move

{ "op": "move", "from": "/biscuits", "path": "/cookies" }

Moves a value from one location to the other. Both from and path are JSON Pointers.

Test

{ "op": "test", "path": "/best_biscuit/name", "value": "Choco Leibniz" }

Tests that the specified value is set in the document. If the test fails, then the patch as a whole should not apply.


pom.xml

There is and interface and and implementation.

The interface is javax.json:javax.json-api (no need to add any explicit dependency)

The implementation requires to add a dependency that implements the interface:

<dependency>

    <!-- JSON Patch implementation of interface javax.json:javax.json-api -->

    <groupId>com.github.java-json-tools</groupId>

    <artifactId>json-patch</artifactId>

    <version>1.13</version>

</dependency>

Note: Another implementation (don't use both at the same time) could be:

<dependency>

    <groupId>org.apache.johnzon</groupId>

    <artifactId>johnzon-core</artifactId>

    <version>1.2.14</version>

</dependency>

Sample DTO

public class Customer {

    private String id;

    private String telephone;

    private List<String> favorites;

    private Map<String, Boolean> communicationPreferences;

}


A JSON representation of an object of the class is:

    "id":"1",

    "telephone":"001-555-1234",

    "favorites":["Milk","Eggs"],

    "communicationPreferences": {"post":true, "email":true}

}


Sample Controller

Remark: The “Content-Type”  must be: application/json-patch+json 

@PatchMapping(path = "/{id}", consumes = "application/json-patch+json")

public ResponseEntity<Customer> 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();

    }

}

The real job (usually in a 'Service') would make all kinds of validations on JsonPatch, etc:

private Customer applyPatchToCustomer(

  JsonPatch patch, Customer targetCustomer) throws JsonPatchException, JsonProcessingException {

    ObjectMapper objectMapper = new ObjectMapper();

    JsonNode patched = patch.apply(objectMapper.convertValue(targetCustomer, JsonNode.class));

    return objectMapper.treeToValue(patched, Customer.class);

}


Sample invocations

Create

curl -i -X POST http://localhost:8080/customers -H "Content-Type: application/json" 

  -d '{"telephone":"+1-555-12","favorites":["Milk","Eggs"],"communicationPreferences":{"post":true,"email":true}}'

Patch

curl -i -X PATCH http://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"}

]'

Patch w/ the map 'communicationPreferences'

curl -i -X PATCH http://localhost:8080/customers/1 -H "Content-Type: application/json-patch+json" -d '[

  {"op":"replace","path":"/communicationPreferences","value": {"post":false, "email":false}},

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

]'

Issue referencing a single key of a map (it says it's fixed):

https://github.com/spring-projects/spring-data-rest/issues/1697