I had the pleasure of modifying a large code base that extensively used Model Mapper. I ran into some issues using it blindly, and I did not have much luck searching for answers. Most of the information in this blog posts already exists elsewhere, but I was suprised that it was not very visible on google. Hopefully we can correct that.
Here are my tips for effective use of ModelMapper.
Use and understand PropertyMap
The first thing you should understand are the nuances of configuring a PropertyMap, which I personally found unintuitive. It wasnt until scouring the documentation and a few well written Stack Overflow posts that everything started to click.
If you’re like me, this first thing you’ll run up against when searching for help is running across an unintuitive example like this:
public class MyMapper extends PropertyMap<Source, Destination>{
@Override
protected void configure(){
map(source.getProperty()).setOther(null);
}
}
The reason why this is confusing, is why would I “set” my destination property to null?
The configure() function contains EDSL
The answer is, the code in the configure()
method is not actually executed as part of your mapping at all. It is actually an Embedded Domain Specific Language (EDSL). This is the number one thing that made my understanding of Model Mapper so much clearer.
You can read more about how the EDSL works in the official javadocs.
This gist is this:
- The mapping is not executed as Java code. The
PropertyMap.configure()
is not being executed during the mapping process. You can validate this by setting a breakpoint in the method while debugging a mapping. - The mapping is interpreted by ModelMapper when the mappings are added using
addMapping(...)
. -
Converter.convert()
’s are executing during runtime. So you can do something like this:
public class MyMapper extends PropertyMap<Source, Destination>{
private Converter<String, String> toUppercase = (c) -> {
return c.getSource().toUpperCase();
};
@Override
protected void configure(){
using(toUppercase)
.map(source.getProperty())
.setOther(null);
}
}
And the code in the toUppercase
converter will be executed each time the objects are mapped.
Use TDD when defining mappings
I cover unit testing in the next section, but bring it up here because it can reduce boilerplate. ModelMapper actually does a pretty good job on its own in most cases. If you define what you expect in tests, it will force you to do the bare minimum definition in the PropertyMap.
For example, if you were to naively define a PropertyMap
without running any tests, you might end up with something as verbose as this:
public class OverkillPropertyMap
extends PropertyMap<PersonDto, Person> {
@Override
protected void configure() {
map(source.getFirstName(), destination.getFirstName());
map(source.getLastName(), destination.getLastName());
map(source.getEmail(), destination.getEmail());
map(source.getPhone(), destination.getPhone());
map(source.getAddress1(), destination.getAddress1());
map(source.getAddress2(), destination.getAddress2());
map(source.getCity(), destination.getCity());
map(source.getState(), destination.getState());
map(source.getZip(), destination.getZip());
map(source.getCountry(), destination.getCountry());
}
}
This is a contrived example that can seem silly to an experienced developer, but you’d be surprised at how many mappings like these pop up in codebases.
Let’s see if we can simplify this in the next section with a unit test.
Unit Testing
The ModelMapper official docs state that you should always unit test mappings, and I fully agree. As with most issues, it’s much easier to catch things in unit tests than later on in the SDLC.
Here are the two things to always cover in your mapping unit tests:
-
Always use
ModelMapper.validate()
- Verify fields are being mapped with assertEquals(…)
ModelMapper.validate()
The handy method will verify that all destination properties are matched. This is extremely useful if somebody forgets to map a destination property after adding it. In other words, this will protect your mapper from future changes to both source/destination objects.
If you are getting false positives here, you can skip properties in your mapping:
skip(destination.getPropertyNotMapped());
Example of effective unit test
Going back to the example from the last section, except let’s start with a unit test this time.
A standard unit test would look something like this:
public class PersonMapTests {
private ModelMapper modelMapper;
@Before
public void setup(){
modelMapper = new ModelMapper();
modelMapper.addMappings(new PersonMap());
modelMapper.getConfiguration()
.setMatchingStrategy(MatchingStrategies.STRICT);
}
@Test
public void map_ShouldValidate_IfObjectsValid(){
var personDto = preparePersonDto();
var personModel =
modelMapper.map(personDto, Person.class);
modelMapper.validate();
assertEquals(
personDto.getFirstName(), personModel.getFirstName());
assertEquals(
personDto.getLastName(), personModel.getLastName());
assertEquals(
personDto.getEmail(), personModel.getEmail());
assertEquals(
personDto.getPhone(), personModel.getPhone());
assertEquals(
personDto.getAddress1(), personModel.getAddress1());
assertEquals(
personDto.getAddress2(), personModel.getAddress2());
assertEquals(
personDto.getCity(), personModel.getCity());
assertEquals(
personDto.getState(), personModel.getState());
assertEquals(
personDto.getCountry(), personModel.getCountry());
assertEquals(
personDto.getZip(), personModel.getZip());
}
private PersonDto preparePersonDto() {
return new PersonDto.Builder()
.firstName("Isaac")
.lastName("Broyles")
.email("isaacbroyles@example.com")
.phone("202-555-0129")
.address1("123 Street")
.address2("Apt 101")
.city("Ville")
.state("TX")
.zip("12345")
.country("USA")
.build();
}
}
In running this unit test, I get an error like this:
1) Unmapped destination properties found in TypeMap[PersonDto -> Person]:
com.isaacbroyles.examples.modelmapperexamples.models.Person.setLastModified()
Uh oh, the validate()
method is showing us that we have a destination property that is not mapped. So let’s define a PropertyMap
to fix that issue.
Here is the property map, with just the minimal code to fix my error:
public class PersonMap extends PropertyMap<PersonDto, Person> {
@Override
protected void configure() {
map(source.getDateCreated(),
destination.getLastModified());
}
}
Now I rerun my test – Oh cool, it passes now.
Compare the PropertyMap
we ended up with to the OverkillPropertyMap
in the previous section. All the other properties were automatically mapped, even with MatchingStrategies.STRICT
.
Conclusion
I’m somewhat conflicted over whether I like the usage of ModelMapper or not. On the one hand, it provides a good way to reduce boilerplate mapping code. On the other hand, some of the rather unintuitive aspects of it have caused me headache. This isn’t necessarily a critique of ModelMapper, and probably speaks more to my inexperience with it than any lacking on the tool’s part. It has obviously grown to be a popular tool for a good reason.
Regardless, I thought this post would prove useful to others who are in a codebase that uses ModelMapper, and are running into the same kinds of issues.
Related Links
- ModelMapper - The official ModelMapper website.
- Excellent StackOverflow answer - I saw this great answer, which prompted me to dig in deeper to get more understanding about ModelMapper’s inner workings.
Top comments (0)