@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");
}
}
@Service
public class JsonPatchService {
private final ObjectMapper mapper;
private final Validator validator;
public JsonPatchService(ObjectMapper mapper, Validator validator) {
this.mapper = mapper;
this.validator = validator;
}
public User apply(User current, JsonNode patchNode) {
JsonNode original = mapper.valueToTree(current);
JsonNode patched;
try {
JsonPatch patch = JsonPatch.fromJson(patchNode);
patched = patch.apply(original);
} catch (JsonPatchException | IOException e) {
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "invalid patch: " + e.getMessage());
}
assertImmutableUntouched(original, patched);
User result;
try {
result = mapper.treeToValue(patched, User.class);
} catch (JsonProcessingException e) {
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "patch produced an invalid document");
}
Set<ConstraintViolation<User>> violations = validator.validate(result);
if (!violations.isEmpty()) {
String detail = violations.stream()
.map(v -> v.getPropertyPath() + " " + v.getMessage())
.collect(Collectors.joining("; "));
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, detail);
}
return result;
}
private void assertImmutableUntouched(JsonNode before, JsonNode after) {
for (String field : List.of("id", "createdAt", "version")) {
if (!Objects.equals(before.get(field), after.get(field))) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "field '" + field + "' is not editable");
}
}
}
}
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Version
private Long version;
@NotBlank
@Size(max = 80)
private String displayName;
@Email
@NotBlank
private String email;
@Min(0)
@Max(150)
private Integer age;
@Column(updatable = false)
private Instant createdAt = Instant.now();
public Long getId() { return id; }
public Long getVersion() { return version; }
public String getDisplayName() { return displayName; }
public void setDisplayName(String displayName) { this.displayName = displayName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public Instant getCreatedAt() { return createdAt; }
}
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
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
package com.example.starter.config;
import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Custom Spring Boot starters
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
package deps
import (
"net"
"net/http"
"time"
HTTP client tuned for production: timeouts, transport, and connection reuse
Share this code
Here's the card — post it anywhere.