Today we are going to have a conversation about REST(ful) services and APIs, more precisely, around one peculiar subject many experienced developers are struggling with. To put things into perspective, we are going to talk about web APIs, where the REST(ful) principles adhere to HTTP protocol and heavily exploit the semantics of HTTP methods and (usually but not necessarily) use JSON to represent the state.
One particular HTTP method stands out, and although its meaning sounds pretty straightforward, the implementation is far from that. Yes, we are looking at you, the PATCH. So what is the problem, really? It is just an update, right? Yes, in the essence the semantics of the PATCH method in the context of the HTTP-based REST(ful) web services is partial update of the resource. Now, how would you do that, Java developer? Here is where the fun begins.
Let us go over a very simple example of book management API, modeled using latest JSR 370: Java API for RESTful Web Services (JAX-RS 2.1) specification (which finally includes the @PATCH annotation!) and terrific Apache CXF framework. Our resource is just a very simplistic Book class.
public class Book { private String title; private Collection>String< authors; private String isbn; }
How would you implement the partial update using the PATCH method? Sadly, the brute force solution, the null
feast, is the clear winner here.
@PATCH @Path("/{isbn}") @Consumes(MediaType.APPLICATION_JSON) public void update(@PathParam("isbn") String isbn, Book book) { final Book existing = bookService.find(isbn).orElseThrow(NotFoundException::new); if (book.getTitle() != null) { existing.setTitle(book.getTitle()); } if (book.getAuthors() != null) { existing.setAuthors(book.getAuthors()); } // And here it goes on and on ... // ... }
In the nutshell, this is null-guarded PUT clone. Probably, someone could claim that it kind of works and declare the victory here. But hopefully for majority of us this approach clearly has a lot of flaws and should be never taken. Alternatives? Yes, absolutely, RFC-6902: JSON Patch, not an official standard just yet but it is getting there.
The RFC-6902: JSON Patch drastically changes the game by expressing a sequence of operations to apply to a JSON document. To illustrate the idea in action, let us start from a simple example of changing book's title, described in the terms of desired outcome.
{ "op": "replace", "path": "/title", "value": "..." }
Looks clean, what about adding the authors? Easy ...
{ "op": "add", "path": "/authors", "value": ["...", "..."] }
Awesome, sold out, but ... implementation-wise it seems to require quite a lot of work, isn't it? Not really if we rely on the latest and greatest JSR 374: Java API for JSON Processing 1.1 which fully supports RFC-6902: JSON Patch. Armed with the right tools, this time let us do it right.
org.glassfish javax.json 1.1.2
Interestingly, not many are aware of that Apache CXF, and in general any JAX-RS-complaint framework, closely integrates with JSON-P and supports its basic data types. In case of Apache CXF, it is just a matter of adding cxf-rt-rs-extension-providers
module dependency:
org.apache.cxf cxf-rt-rs-extension-providers 3.2.2
And registering JsrJsonpProvider with your server factory bean, for example:
@Configuration public class AppConfig { @Bean public Server rsServer(Bus bus, BookRestService service) { JAXRSServerFactoryBean endpoint = new JAXRSServerFactoryBean(); endpoint.setBus(bus); endpoint.setAddress("/"); endpoint.setServiceBean(service); endpoint.setProvider(new JsrJsonpProvider()); return endpoint.create(); } }
With all the pieces wired together, our PATCH operation could be implemented using JSR 374: Java API for JSON Processing 1.1 alone, in just a few lines:
@Service @Path("/catalog") public class BookRestService { @Inject private BookService bookService; @Inject private BookConverter converter; @PATCH @Path("/{isbn}") @Consumes(MediaType.APPLICATION_JSON) public void apply(@PathParam("isbn") String isbn, JsonArray operations) { final Book book = bookService.find(isbn).orElseThrow(NotFoundException::new); final JsonPatch patch = Json.createPatch(operations); final JsonObject result = patch.apply(converter.toJson(book)); bookService.update(isbn, converter.fromJson(result)); } }
The BookConverter performs the conversion between Book class and its JSON representation (and vise versa), which we are doing by hand to illustrate another capabilities which JSR 374: Java API for JSON Processing 1.1 provides.
@Component public class BookConverter { public Book fromJson(JsonObject json) { final Book book = new Book(); book.setTitle(json.getString("title")); book.setIsbn(json.getString("isbn")); book.setAuthors( json .getJsonArray("authors") .stream() .map(value -> (JsonString)value) .map(JsonString::getString) .collect(Collectors.toList())); return book; } public JsonObject toJson(Book book) { return Json .createObjectBuilder() .add("title", book.getTitle()) .add("isbn", book.getIsbn()) .add("authors", Json.createArrayBuilder(book.getAuthors())) .build(); } }
To finish up, let is wrap this simple JAX-RS 2.1 web API into the beautiful Spring Boot envelope.
@SpringBootApplication public class BookServerStarter { public static void main(String[] args) { SpringApplication.run(BookServerStarter.class, args); } }
And run it.
mvn spring-boot:run
To conclude the discussion, let us play a bit with more realistic examples by deliberately adding an incomplete book into our catalog.
$ curl -i -X POST http://localhost:19091/services/catalog -H "Content-Type: application\json" -d '{ "title": "Microservice Architecture", "isbn": "978-1491956250", "authors": [ "Ronnie Mitra", "Matt McLarty" ] }' HTTP/1.1 201 Created Date: Tue, 20 Feb 2018 02:30:18 GMT Location: http://localhost:19091/services/catalog/978-1491956250 Content-Length: 0
There are a couple of inaccuracies we would like to fix in this book description, namely set the title to be complete, "Microservice Architecture: Aligning Principles, Practices, and Culture", and include missing co-authors, Irakli Nadareishvili and Mike Amundsen. With the API we have developed a moment ago, it is a no-brainer.
$ curl -i -X PATCH http://localhost:19091/services/catalog/978-1491956250 -H "Content-Type: application\json" -d '[ { "op": "add", "path": "/authors/0", "value": "Irakli Nadareishvili" }, { "op": "add", "path": "/authors/-", "value": "Mike Amundsen" }, { "op": "replace", "path": "/title", "value": "Microservice Architecture: Aligning Principles, Practices, and Culture" } ]' HTTP/1.1 204 No Content Date: Tue, 20 Feb 2018 02:38:48 GMT
The path reference of the first two operations may look confusing a bit but fear no more, let us clarify that. Because authors
is a collection (or in terms of JSON data types, an array) we could use RFC-6902: JSON Patch array index notation to specify exactly where we would like the new element to be inserted. The first operations uses index '0'
to denote the head position, while the second one uses '-'
placeholder to simplify say "add to the end of the collection". If we retrieve the book right after the update, we should see our modifications to be applied exactly as we asked.
$ curl http://localhost:19091/services/catalog/978-1491956250 { "title": "Microservice Architecture: Aligning Principles, Practices, and Culture", "isbn": "978-1491956250", "authors": [ "Irakli Nadareishvili", "Ronnie Mitra", "Matt McLarty", "Mike Amundsen" ] }
Clean, simple and powerful. To be fair, there is a price to pay is a form of additional JSON manipulations (in order to apply the patch) but is it worth the effort? I believe it is ...
Next time you are going to design new shiny REST(ful) web APIs, please seriously consider RFC-6902: JSON Patch to back the PATCH implementation of your resources. I believe more close integration with JAX-RS is also coming (if not there yet) to directly support JSONPatch class and its family.
And last, but not least, in this post we have touched on server-side implementation only, but JSR 374: Java API for JSON Processing 1.1 includes convenient client-side scaffolding as well, giving full-fledged programmatic control over the patches.
final JsonPatch patch = Json.createPatchBuilder() .add("/authors/0", "Irakli Nadareishvili") .add("/authors/-", "Mike Amundsen") .replace("/title", "Microservice Architecture: Aligning Principles, Practices, and Culture") .build();
The complete project sources are available on Github.