Using MapStruct to implement PATCH endpoints in Spring Boot web applications
Background
I recently started working on a Spring Boot Web-based project that necessitates a PATCH endpoint on its API to enable modification of specific fields in a persisted entity. In this scenario, the content in the request body is the “patch source”, and the underlying entity being patched is the “patch target”.
If at all possible, I wanted to avoid doing any sort of manual bean mapping or modification at all costs (e.g. manually using the setters on the entity being patched to set the values from the PATCH request), because the entity in question is quite hefty and this would have resulted in a lot of boilerplate for such a “simple” task.
The project in question also uses request and response objects generated from an OpenAPI specification via a Maven plugin, so it would be preferable not to have to update the patching logic if the underlying spec were to change.
How hard can that be?
Research
During my research, a lot of the potential solutions I explored for this online seemed to have drawbacks that make them unworkable for my use case. For example:
Implementing the JSON Patch standard
This seemed promising at first, the JSON Patch standard (defined in RFC 6902) gives the consumer of the API a lot of control over how the underlying entity is patched. A JSON Patch-conformant request is simply a JSON array of operations that are performed on the underlying entity to produce a result.
There are a few Java libraries available to handle JSON Patch requests, but they all seem to suffer from a few common problems; the biggest deal-breaker for my use case is that it’s relatively complicated to restrict which operations can be performed on which fields. It’s possible, but it results in a lot of boilerplate that is tightly coupled to the underlying implementation of both the patch target and the patch source.
It’s also harder to perform validation on the values contained within the request before the patch target is updated; all of the libraries I looked at are designed such that you can’t validate the patch request upfront (e.g. using JSR-380 bean validation and the @Validated annotation on the @RequestBody argument of your controller method) - you would have to check this manually, which again, creates boilerplate that’s coupled to the underlying implementation of the patch source/target.
JSON Patch is probably fine if you don’t want to have fine-grained control over how an entity can be patched, but for more restrictive use cases it can get out of control complexity-wise. If you get this wrong, it can have nasty data integrity implications that you might not have thought about - imagine if the consumer of your API could update or remove the ID field on a JPA entity, for instance. All of the relations to that row in the underlying table would break!
Use an object mapper like Jackson
This can also work quite well in simpler use cases; one could argue that the majority of Spring Boot Web projects already have some sort of object mapper available, and the fact that the patch source is a Java class makes it easy to restrict which fields can be patched - just exclude them from the patch source.
It’s also easier to validate the contents of the patch source using techniques like JSR-380 bean validation, enabling you to do “sensible” validation of PATCH requests.
I ultimately decided against this approach for my use case for a couple of reasons:
- At least with Jackson, the behaviour of the object mapper is largely controlled by annotations on the mapping source/target. This makes it tricky to control the behaviour of the object mapper without creating another differently-configured instance if you don’t control the implementation of the patch source or target. In my case, the request object (the patch source in this scenario) is generated from an OpenAPI specification, so I can’t easily add Jackson annotations to it. Maybe there are other object mappers that don’t suffer from this problem, but bringing such a large dependency into the project for such a trivial use case feels like overkill.
- This is somewhat down to personal preference, but I’m not a massive fan of using object mappers to transform POJOs as a general rule. Their behaviour can be difficult to reason about, as it can be influenced by a number of factors (annotations on source/target classes, ObjectMapper configurations, etc.) - not only does this make it harder to debug if something goes wrong, the associated complexity also has a performance penalty.
Use reflection to update the fields on the patch target
For reasons I am yet to fathom, I saw more than one solution floating around online that involved using reflection to iterate through all of the fields of the patch source, and apply the new values to any matching fields in the patch target. I struggle to see any advantages of doing it this way, but the disadvantages are obvious and numerous:
- Using reflection for this purpose violates OOP best practices like encapsulation - unless you are really careful with your implementation to detect and use setters in the patching target. Reflection lets you do insidious things like modify private fields, which may violate assumptions and assertions made elsewhere in your application.
- Reflection is difficult to implement right, especially in a maintainable way, and especially if you potentially have lots of different patch source/target types to deal with.
- Reflection is slow (because types have to be resolved at runtime), and using reflection to modify prevents the JVM from performing certain optimisations on generated bytecode.
- Using reflection for this purpose skirts type safety guarantees made by the compiler, in particular around generic types:
- In Java, generic type arguments are erased at compile time, e.g.
List<String>effectively becomesList<Object>in the generated bytecode. - The compiler will prevent you from violating any type constraints for a given instance of a generic class (e.g. you can’t add an Integer to a
List<String>) - but this can’t be done at runtime, because information about those type constraints no longer exists. - This means that, for example, you could use reflection to replace a
List<String>with aList<Integer>- you wouldn’t know about this until another part of your application attempts to access a value in that list and aClassCastExceptionis thrown, because what it thought was a list of Strings has now suddenly become a list of Integers. This is the type of bug that’s really hard to track down.
- In Java, generic type arguments are erased at compile time, e.g.
In the unlikely event that you’ve read all of the above and are still considering using reflection to patch POJOs and entities… why?
The solution
While reading about object mappers in Java and how to wrangle them correctly, I came across a library called MapStruct. The more I read, the more it seemed to tick the boxes:
- It generates extremely minimal mapping implementations at compile time using an annotation processor, making the resulting code really fast and removing the need for additional runtime dependencies.
- It uses interfaces for configuring the mapping behaviour, rather than annotations on the source/target classes or some externalised configuration. The configuration is quite flexible too - you have a lot of control over how the mapping works and how the underlying implementation is generated.
- It respects encapsulation, to a degree; you can configure it to automatically detect Lombok setters (or custom setters if you prefer).
- The Lombok binding requires a separate annotation processor but this is only used at compile time.
- Its behaviour is really easy to reason about - if you want to understand what it’s doing, the implementation can be found in
target/generated-sources. - Because the mapping source is just a POJO, you can use JSR-380 validation to sensibly validate the request upfront.
- It can automatically mark generated mapping implementations as a Spring
@Component, allowing them to be discovered by Spring Boot autoconfiguration without having to explicitly declare a bean.
This is ultimately the solution I landed on - it was super simple to implement and worked like a charm for my use case.
Adding the dependencies to the POM
There’s a single dependency for MapStruct that contains the various annotations for configuring Mappers:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
You will also need to configure the Maven compiler plugin to run the annotation processor for MapStruct (and, optionally, Lombok and the associated MapStruct binding if you wish to use Lombok):
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<!-- The below annotation processors are only needed if using Lombok: -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
The mapstruct-processor annotation processor is responsible for generating the mapper implementations based on the Mapper interfaces it detects in your application.
Implementing a PATCH endpoint
To begin…
Imagine we had a JPA entity that looked something like this:
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
/*
* Never actually use sequential IDs for sensitive data like this.
* It opens you up to enumeration attacks; this is just an example.
*/
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstName;
private String surname;
private String email;
private String phoneNumber;
private boolean isAdministrator;
}
If we wanted to update the firstName, surname, email and phoneNumber fields, you might also have a request POJO that looks like this:
@Builder
public record PatchUserRequest(
// You can of course add JSR-380 bean validation annotations here if you wish:
@Nullable String firstName,
@Nullable String surname,
@Nullable String email,
@Nullable String phoneNumber
) {}
The individual fields are marked @Nullable to denote that they can be omitted from the request. If they are omitted, they remain null - the MapStruct mapper can later be configured to ignore these and not patch the value if so.
Finally, that would tie into a controller method that looks something like this:
@PatchMapping("/v1/user/{userId}")
public ResponseEntity<Void> patchUser(@PathVariable String userId, @Validated @RequestBody PatchUserRequest patchUserRequest) {
// ...
}
Creating the mapper
The entire mapper configuration looks like this:
@Mapper(
componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE
)
public interface PatchUserMapper {
User patchUser(@MappingTarget User user, PatchUserRequest patchUserRequest);
}
To break it down a little further:
- The
@Mapperannotation denotes that this interface should be treated as a MapStruct mapper.componentModel = "spring"tells the MapStruct code generator to generate an implementation that’s compatible with Spring - the generated implementation has an@Componentannotation so it can be autoconfigured.nullValuePropertyMappingStrategydefines the behaviour if a property/field in the mapping source is null or not present. For a PATCH endpoint, the (usually) sensible behaviour is to just ignore it and not update the mapping target.
- The interface has a method,
patchUser, which takes two arguments:- The user being patched - this is annotated with
@MappingTargetas a hint to the annotation processor that the implementation should modify whichever object is passed here. - The patch source, in this case
PatchUserRequest.
- The user being patched - this is annotated with
MapStruct is much more powerful than this blog post lets on - but this is not intended to be a MapStruct tutorial, more of a guide on how you can use it to implement PATCH endpoints.
When the project is compiled (e.g. using mvn compile), the following mapper implementation is generated:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2026-06-05T01:17:28+0100",
comments = "version: 1.6.3, compiler: javac, environment: Java 25 (Homebrew)"
)
@Component
public class PatchUserMapperImpl implements PatchUserMapper {
@Override
public User patchUser(User user, PatchUserRequest patchUserDto) {
if ( patchUserDto == null ) {
return user;
}
if ( patchUserDto.firstName() != null ) {
user.setFirstName( patchUserDto.firstName() );
}
if ( patchUserDto.surname() != null ) {
user.setSurname( patchUserDto.surname() );
}
if ( patchUserDto.email() != null ) {
user.setEmail( patchUserDto.email() );
}
if ( patchUserDto.phoneNumber() != null ) {
user.setPhoneNumber( patchUserDto.phoneNumber() );
}
return user;
}
}
This implementation is about as simple as can be IMO.
Using the mapper to patch a JPA entity
The service layer implementation is quite straightforward (imagine that the controller calls this method in the service layer):
@Service
@RequiredArgsConstructor
public class PatchUserServiceImpl implements PatchUserService {
private final PatchUserMapper patchUserMapper;
private final UserRepository userRepository;
public void patchUser(String userId, PatchUserRequest patchUserRequest) {
// Get the user entity to patch:
User targetUser = this.userRepository.findById(userId).orElseThrow(UserNotFoundException::new);
// Use the mapper to patch the entity:
targetUser = this.patchUserMapper.patchUser(targetUser, patchUserRequest);
// Save the patched user back to the DB:
this.userRepository.save(targetUser);
}
}
Usage
Suppose there is an entity in the database with the following fields, per the User entity example from earlier in this post:
{
"id": 1,
"firstName": "John",
"surname": "Doe",
"email": "john.doe@example.com",
"phoneNumber": "+447700900123",
"isAdministrator": false
}
If you fire off a request to the endpoint that looks something like this:
PATCH /v1/user/1
{
"firstName": "Jane",
"email": "jane.doe@example.com"
}
The underlying entity should be updated to look like this:
{
"id": 1,
"firstName": "Jane",
"surname": "Doe",
"email": "jane.doe@example.com",
"phoneNumber": "+447700900123",
"isAdministrator": false
}
Of course, it’s a wise idea to cover this with automated tests, just to make sure the mapping behaviour works as expected and doesn’t regress if you change the mapper configuration.
Conclusion
I hadn’t expected there to be so many ways to implement a single PATCH endpoint, all (most) of which seem to have their own drawbacks and advantages over one another. The MapStruct solution described in this blog post has been working like a charm for me, although if you dislike the idea of compile-time code generation (and there are some valid reasons for this), it might not be the right approach for you.