Thursday, June 3, 2021

Spring Endpoint to handle Json Patch and Json Merge Patch

In a previous blog post I went over the basics of Json Patch and Json Merge Patch and how a code that performs these operations looks like. In this post I will go over the details of how to expose a Spring based endpoint to accept a Json Patch or Json Merge Patch body and patch and save an entity. The entity that I want to update is a Book, and a sample book looks like this in a json form:
{
  "title": "Goodbye!",
  "author": {
    "givenName": "John",
    "familyName": "Doe"
  },
  "tags": [
    "example",
    "sample"
  ],
  "content": "This will be unchanged"
}
A kotlin representation of this entity is the following:
data class Book(
    val title: String,
    val author: Author,
    val tags: List<String>,
    val content: String,
    val phoneNumber: String? = null
)

data class Author(
    val givenName: String,
    val familyName: String? = null
)
Let's start with an endpoint that performs a Json Patch The endpoint should accept the patch in a request body, should accept a content type of "application/json-patch+json": A sample kotlin code of such an endpoint is the following:
import com.github.fge.jsonpatch.JsonPatch
...
...
@PatchMapping(path = ["/{id}"], consumes = ["application/json-patch+json"])
fun jsonPatchBook(
    @PathVariable id: String,
    @RequestBody patch: JsonNode
): Mono<ResponseEntity<Book>> {
    return Mono.fromSupplier {
        val jsonPatch: JsonPatch = JsonPatch.fromJson(patch)
        val original: JsonNode = objectMapper.valueToTree(getBook(id))
        val patched: JsonNode = jsonPatch.apply(original)
        val patchedBook: Book =
            objectMapper.treeToValue(patched) ?: throw RuntimeException("Could not convert json back to book")
        updateBook(patchedBook)
        ResponseEntity.ok(patchedBook)
    }
}
All that is involved is to : 
  1. Take in the Json Patch body and convert it into the JsonPatch type 
  2. Retrieve the Book entity for the identifier 
  3. Convert the Book entity into a Json representation 
  4. Apply the patch and convert the resulting json back into the Book entity

For an endpoint that performs Json Merge patch, along the same lines, the endpoint should accept the json merge patch request body with a content type of "application/merge-patch+json":


@PatchMapping(path = ["/{id}"], consumes = ["application/merge-patch+json"])
fun jsonMergePatchBook(
    @PathVariable id: String,
    @RequestBody patch: JsonNode
): Mono<ResponseEntity<Book>> {
    return Mono.fromSupplier {
        val original: JsonNode = objectMapper.valueToTree(getBook(id))
        val patched: JsonNode = JsonMergePatch.fromJson(patch).apply(original)
        val patchedBook: Book =
            objectMapper.treeToValue(patched) ?: throw RuntimeException("Could not convert json back to book")
        updateBook(patchedBook)
        ResponseEntity.ok(patchedBook)
    }
}
Steps are:
  1. Take in the Json Merge Patch body
  2. Retrieve the Book entity for the identifier 
  3. Convert the Book entity into a Json representation 
  4. Apply the merge patch and convert the resulting json back into the Book entity
All fairly straightforward thanks to the easy way that Spring Web allows to expose an endpoint and the way json-patch library provides support for the Json Patch and Json Merge Patch operations. If you need a complete working example with all the dependencies pulled in, here is a sample in my github repository - https://github.com/bijukunjummen/coroutine-cities-demo/blob/main/src/test/kotlin/samples/geo/patch/BookController.kt