java 147 lines · 4 tabs

Spring Boot PATCH Endpoint With JSON Merge Patch (RFC 7386)

Shared by codesnips Sep 2026
4 tabs
@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductRepository repository;
    private final JsonMergePatchService patchService;

    public ProductController(ProductRepository repository, JsonMergePatchService patchService) {
        this.repository = repository;
        this.patchService = patchService;
    }

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

        Product merged = patchService.applyPatch(patch, current, Product.class);
        merged.setId(id); // id is immutable regardless of what the patch tried

        return ResponseEntity.ok(repository.save(merged));
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<String> onInvalid(ConstraintViolationException ex) {
        String detail = ex.getConstraintViolations().stream()
                .map(v -> v.getPropertyPath() + " " + v.getMessage())
                .collect(Collectors.joining("; "));
        return ResponseEntity.unprocessableEntity().body(detail);
    }
}
4 files · java Explain with highlit

This snippet implements a proper partial-update endpoint using the HTTP PATCH method with JSON Merge Patch semantics as defined by RFC 7386. Unlike a PUT, which replaces the entire resource, a merge patch describes only the fields that should change, and a JSON null explicitly clears a field. That distinction — omitted vs. present-but-null — is the whole reason a naive @RequestBody Dto approach fails: Jackson cannot tell an absent key from one deserialized to null, so the server can't know whether the client meant "leave it alone" or "erase it".

The ProductController accepts the request with content type application/merge-patch+json and binds the raw body as a JsonMergePatch from the jakarta.json API, deliberately avoiding a typed DTO at the edge. It loads the current entity, delegates the actual merge to JsonMergePatchService, and validates the merged result before persisting. Returning 409 Conflict-free behavior is achieved by re-validating post-merge rather than pre-merge.

In JsonMergePatchService, the current entity is serialized to a JsonValue, the patch is applied with patch.apply(target), and the resulting JSON is deserialized back into a fresh instance of the target class. This round-trip via Jackson and jakarta.json keeps the merge logic entirely generic — the same applyPatch method works for any type, so no per-field mapping code is required. The service also runs Bean Validation through a Validator so that a patch cannot push the entity into an invalid state, throwing a ConstraintViolationException that the controller advice translates.

The Product entity uses standard jakarta.validation constraints; note that @NotBlank on name means a client cannot null it out, while a nullable description may legitimately be set to null. The trade-off of this approach is the double serialization cost and the need for a Jackson ObjectMapper configured to ignore unknown properties. It shines when resources have many optional fields and clients want to send minimal diffs. Pitfalls to watch: merge patch cannot address array elements individually (use JSON Patch / RFC 6902 for that), and the Content-Type must be negotiated correctly or the framework will reject the body.


Related snips

graphql
type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
    createdAt: String!

GraphQL API with Spring Boot

java graphql spring-boot
by David Kumar 3 tabs
ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs
java
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

java spring-boot starter
by David Kumar 4 tabs
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form Validation Example</title>
  <style>

HTML forms with validation and accessibility

html forms validation
by Alex Chang 1 tab
java
package com.example.demo.config;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;

Messaging with Apache Kafka

java kafka messaging
by David Kumar 3 tabs
typescript
import axios from 'axios';

export type NormalizedErrors = {
  fields: Record<string, string>;
  formLevel: string | null;
};

Frontend: normalize and display server validation errors

ux typescript react
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Spring Boot PATCH Endpoint With JSON Merge Patch (RFC 7386) — share card
Link copied