Java Mapping Frameworks
Mapping request DTOs to entities is almost always a smell, it means your entities are nothing more than dumb setter bags and you’re only doing CRUD. Should you really have to do it, it’s way better to code the mapping logic by hand rather than using a framework: trivial and way more flexible. The same goes for response DTOs given query results rarely come from a single entity.
There is no need for an object mapping framework. They are a solution looking for a problem. Yes it is kind of handy if all your field names are the same, but that almost never happens and then you end up with a stack of @Mapping annotations on your method. In practice what that does is move your mapping from nice type-safe java code to raw strings in annotations.
If you do need to map one object to another just hand write static factory methods, then no build time code generation is needed. Keep it simple.
Just use static method, constructor, builder method owned by object. Anything is better than unnecessary dependencies.
Just hand-write factories/translators to map between layers, and then unit test the hell out of them.
The problem
class UserEntity {
private String name;
private int age;
// setters and getters via Lombok
}
class UserDTO {
private String name;
private int age;
// setters and getters via Lombok
}
Options
- MapStruct
- JMapper
- Orika
- ModelMapper
- Dozer
- dOOv
- reMap
- Apache Commons BeanUtils
- Spring BeanUtils - https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/BeanUtils.html
Issues to keep in mind about using mapping frameworks
- Fields stopped mapping silently.
- Debugging was a guessing game.
- Nested mappings turned into nightmares.
Ultimately, what do we want?
Compile-time checking. Clear. Explicit. Fast.
No reflection, no runtime surprises. Just predictable, readable, actual mapping.
- Less boilerplate
- Zero IDE issues. Avoid weird autocompletion bugs.
- Better onboarding for new devs.
- Compile-time safety. Catching mapping errors before they bite us in prod.
Using lombok
UserDTO dto = modelMapper.map(userEntity, UserDTO.class);
Using MapStruct
@Mapper
public interface UserMapper {
UserDTO toDto(UserEntity user);
}
Mapstruct is compile time/generated java code. You can debug it, its java code, no crazy reflection/runtime based mapping, performant.
What I don’t like about MapStruct is that you have to explicitly write mapping interfaces for every pair of mappings you do. This is inevitable of course, as it has to know which mapping code should be generated, but still a bit cumbersome.
After using MapStruct on a large project at a large enterprise company, I cannot really recommend it. For a couple reasons:
-
The documentation is non-existent
-
Mappers cannot be nested more than one level.
-
Mapping sources and targets must parallel each other exactly. I can map `Source -> SourceDto` but not `Source -> OtherSourceDto`. This is frustrating because sometimes an object in your domain must produce multiple display items. Which brings me to…
-
Too much magic. It’s not always clear how the mapping implementation is generated. One example of how this can lead to frustration is how the library handles fields prefixed with “add”. MapStruct assumes that any field that begins with “add” is a builder method (used literally to add a singular object to a collection) and ignores it. That means that if you have a field that genuinely begins with the characters “a-d-d” and is camelcase, MapStruct will ignore the field entirely. There is no way to change this behavior – it led to a wild bug chase and ended with us changing the name of the field altogether. In other projects that might not have been possible.
Apache Commons BeanUtils
- https://dev.to/markyu/advanced-java-simplifying-object-property-copy-and-manipulation-with-beanutil-3l2n
- https://www.baeldung.com/apache-commons-beanutils