java 112 lines · 3 tabs

Applying RFC 6902 JSON Patch with Optimistic Locking in Spring Boot

Shared by codesnips Aug 2026
3 tabs
@RestController
@RequestMapping("/users")
public class UserPatchController {

    private final UserRepository users;
    private final JsonPatchService patchService;

    public UserPatchController(UserRepository users, JsonPatchService patchService) {
        this.users = users;
        this.patchService = patchService;
    }

    @PatchMapping(path = "/{id}", consumes = "application/json-patch+json")
    public ResponseEntity<User> patch(@PathVariable Long id, @RequestBody JsonNode patch) {
        User current = users.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "user not found"));

        User patched = patchService.apply(current, patch);
        User saved = users.save(patched);
        return ResponseEntity.ok(saved);
    }

    @ExceptionHandler(OptimisticLockingFailureException.class)
    public ResponseEntity<String> onConflict() {
        return ResponseEntity.status(HttpStatus.CONFLICT).body("resource was modified concurrently");
    }
}
3 files · java Explain with highlit

This snippet shows how a Spring Boot service applies an RFC 6902 JSON Patch to a persisted resource while keeping validation, immutability rules, and concurrency control intact. JSON Patch is the application/json-patch+json media type: the request body is an array of operations (add, remove, replace, move, copy, test) that describe how to transform a document. Compared to PUT (full replacement) or ad-hoc merge patches, JSON Patch is precise and order-sensitive, which makes it well suited to editing deeply nested fields without shipping the whole object.

In UserPatchController, the endpoint consumes the correct media type and accepts a raw JsonNode rather than a typed body, because a patch document is not the resource — it is a set of operations against it. The controller resolves the entity, delegates the actual patch application to JsonPatchService, and returns the updated representation. Keeping the controller thin means the patch semantics live in one testable place.

JsonPatchService performs the core dance. It converts the entity to a JsonNode, wraps the incoming array in a JsonPatch via JsonPatch.fromJson, and calls apply to produce a patched tree. A malformed patch (bad op, missing path, failed test) throws JsonPatchException, which is translated into a 422 rather than leaking a stack trace. The patched tree is then converted back into a User with treeToValue.

Crucially, the code guards against clients patching fields they should not touch. assertImmutableUntouched compares the id and createdAt between the original and patched trees, rejecting any attempt to rewrite server-owned data. After that, validator.validate runs Bean Validation constraints so the patched object still satisfies the same rules a normal create would. Only then is the entity mutated and saved.

The @Version field on User in User entity adds optimistic locking: if two patches race, the second save fails with an OptimisticLockException instead of silently clobbering changes. The trade-off of JSON Patch is complexity — clients must build correct pointer paths, and test operations are needed to make edits conditional — but in return partial updates become explicit, auditable, and safe. This pattern is the right reach when resources are large, edits are surgical, and lost-update bugs matter.


Related snips

Share this code

Here's the card — post it anywhere.

Applying RFC 6902 JSON Patch with Optimistic Locking in Spring Boot — share card
Link copied