Model mapping in multilayered applications
Rishi Singh | July 1, 2016
Today’s application solutions are modularized into multiple layers to provide logical problem-solving and separation of concerns. Service layers traditionally house business logic, while sentinel layers communicate with front-end clients or external APIs/databases. Across these various components, objects must be transferred consistently and correctly. If your solution involves two or more models with a large set of objects, mapping these objects can be very cumbersome. ModelMapper offers a quick and simple solution to this problem.
Let’s look at a scenario where a user attempts to register with our application:
- The user enters his or her profile information in the front-end
- The front end makes a REST call to our back-end endpoint. As shown in the diagram above, our back-end is broken into three independent layers
- The web layer of our back-end intercepts the request and maps the JSON request to a registration object. For this example, we can assume the registration object contains a person sub-object, which we can call PersonVO
- The web controller calls the registration service, which houses our app business logic. We leverage the ModelMapper to map all fields from a PersonVO object to a service-specific, person-domain object
- This person-domain object can be manipulated based on our business logic, then pushed forward to the data layer for preparation to commit to a database or LDAP for user profile creation
- The person-domain object is now model mapped to a PersonDTO object. This object is data-layer aware and might contain properties that are required by the database schema
Abstracting out the various layers on the back end, the diagram below provides a high level skeleton of the mapped data models:
Using ModelMapper, we can hop across all these layers without concerning ourselves too much in how the models should be mapped. This method offers a clean and consistent approach to propagating data from the surface of our application to its lowest depths.
ModelMapper is primarily available for Java and its various flavors, but other technologies like Scala and Node have similar solutions available. This post focuses on ModelMapper for a Java-based solution, but these core ideas are preserved across all technologies. Dependencies on this library can be imported quite easily using Maven or Gradle.
Configuration and matching strategy
One of the most important aspects of the initial setup is configuring ModelMapper to behave in a reliable and predictable manner to map your various data models.
The default out-of-the-box configuration of ModelMapper will only match on public source/destination methods that are named according to the standard JavaBeans conventions. For more information on JavaBeans conventions, check out Oracle’s documentation.
Customizing this configuration is quite simple—the appropriate properties can simply be enabled or disabled using the corresponding setters. For example, it’s common practice to allow matching on private fields. The code snippet below depicts this example for a pre-initialized ModelMapper object:
More details on configuration can be found here.
The default matching strategy provided by ModelMapper enforces the following constraints:
- Attributes will match in any order
- All source/destination properties must be matched
The second point is important, as the mapper will fail on runtime (if configured), if a source or destination property is not matched. While the matching strategy is configurable, the authors of ModelMapper recommend the default mapping strategy for your solution.
More details on matching strategies can be found here.
In situations where an implicit mapping is insufficient, ModelMapper provides a method to explicitly define mappings.
A converter allows custom conversions from a source to destination property, possibly based on certain criteria. Let’s consider a situation where an error object should only be mapped if present:
This converter converts an ErrorDTO object to an Error, only if the error DTO object is non-null. Otherwise, this object will never be mapped. More details on converters can be found here.
As the name suggests, this feature allows for mapping destination properties based on certain criteria. This snippet sets up a condition based on the presence of an error object:
This returns true or false based on whether an error object is present. We can leverage the converter described above to map the error fields off of this condition.
Conditionals can also be combined to solve complex criteria. More information on conditionals can be found here.
ModelMapper uses TypeTokens to map generic types. Let’s take a simple example, where we map from a list of integers to a list of strings:
As the comment suggests, this will not work because type information is erased at runtime. The alternative is to create an anonymous subclass of TypeToken, passing List<String> as a parameter:
Matching and mapping processes
ModelMapper is divided into two separate processes—a matching process and a mapping process. While most of the matching algorithm is outside the scope of this post, ambiguity handling is an important section to consider.
Let’s look at the following scenario:
In this scenario, PersonDTO has an extra attribute secondLastName that is not contained within Person. So, when we attempt to map Person → PersonDTO:
A validation exception will be thrown in the scenario above, because the destination property secondLastName in PersonDTO, matches no source property in Person.
ModelMapper will attempt to resolve the ambiguity implicitly, but in the event it cannot be resolved, an exception is thrown. A couple of ways in which this exception can be resolved are:
1. Skipping this attribute, with the hope that it will be mapped by another mapper
2. Explicitly setting this attribute
This exception can be ignored using the ignoreAmbiguity flag which is set during the configuration phase, but this isn’t recommended. Setting this flag will lead to attributes not getting mapped, and ModelMapper will consume any runtime exceptions stemming from validation/configuration. This makes debugging failed mappings difficult.
Once the ambiguities are resolved, the second process—the mapping process—kicks in. The mapping algorithm is structured in terms of the below:
- A TypeMap for the mapping is given top priority
- If a converter exists that can convert the source to the destination, it’s given lower priority
- If a TypeMap and converter don’t exist, ModelMapper will implicitly map out the objects
More information on explicit mappings is here.
ModelMapper vs. Dozer
I’ve focused on ModelMapper in this post, but it’s important to compare it to other libraries that aid in cross-model mappings. Dozer is another popular library available, but unlike the static compilation that ModelMapper leverages, Dozer relies on external XML files to pull its configuration during runtime. Performance tests against Dozer have also produced superior results for Dozer alternatives, as this blog post suggests.
With a general understanding of ModelMappers, you can hit the ground running and create a more efficient process of mapping larger data models across multiple layers of your application.
Rishi Singh is a Senior Engineer at Slalom’s Cross-Market delivery center in Chicago. He helps deliver quality products to clients through best practices and industrial standards.