Guide to ModelMapper
Setting Up
If you’re a Maven user just add the modelmapper
library as a dependency:
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.0.0</version>
</dependency>
Otherwise you can download the latest ModelMapper jar and add it to your classpath.
Mapping
Let’s try mapping some objects. Consider the following source and destination object models:
Source model
// Assume getters and setters on each class
class Order {
Customer customer;
Address billingAddress;
}
class Customer {
Name name;
}
class Name {
String firstName;
String lastName;
}
class Address {
String street;
String city;
}
Destination Model
// Assume getters and setters
class OrderDTO {
String customerFirstName;
String customerLastName;
String billingStreet;
String billingCity;
}
We can use ModelMapper to implicitly map an order
instance to a new OrderDTO
:
ModelMapper modelMapper = new ModelMapper();
OrderDTO orderDTO = modelMapper.map(order, OrderDTO.class);
And we can test that properties are mapped as expected:
assertEquals(order.getCustomer().getName().getFirstName(), orderDTO.getCustomerFirstName());
assertEquals(order.getCustomer().getName().getLastName(), orderDTO.getCustomerLastName());
assertEquals(order.getBillingAddress().getStreet(), orderDTO.getBillingStreet());
assertEquals(order.getBillingAddress().getCity(), orderDTO.getBillingCity());
How It Works
When the map
method is called, the source and destination types are analyzed to determine which properties implicitly match according to a matching strategy and other configuration. Data is then mapped according to these matches.
Even when the source and destination objects and their properties are different, as in the example above, ModelMapper will do its best to determine reasonable matches between properties according to the configured matching strategy.
Handling Mismatches
While ModelMapper will do its best to implicitly match source and destination properties for you, sometimes you may need to explicitly define mappings between properties.
ModelMapper supports a variety of mapping approaches, allowing you to use any mix of methods and field references. Let’s map Order.billingAddress.street
to OrderDTO.billingStreet
and map Order.billingAddress.city
to OrderDTO.billingCity
:
modelMapper.typeMap(Order.class, OrderDTO.class).addMappings(mapper -> {
mapper.map(src -> src.getBillingAddress().getStreet(),
Destination::setBillingStreet);
mapper.map(src -> src.getBillingAddress().getCity(),
Destination::setBillingCity);
});
Conventional Configuration
As an alternative to manually mapping Order.billingAddress.street
to OrderDTO.billingStreet
, we can configure different conventions to be used when ModelMapper attempts to match these properties. The Loose matching strategy, which more loosely matches property names, will work in this case:
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.LOOSE);
Additional matching strategies and other conventions can also be configured to control ModelMapper’s property matching process.
Validating Matches
Aside from writing tests to verify that your objects are being mapped as expected, ModelMapper has built-in validation that will let you know if any destination properties are unmatched.
First we have to let ModelMapper know about the types we want to validate. We can do this by creating a TypeMap
:
modelMapper.createTypeMap(Order.class, OrderDTO.class);
Or we can add mappings from a PropertyMap
which will automatically create a TypeMap
if one doesn’t already exist for the source and destination types:
modelMapper.addMappings(new OrderMap());
Then we can validate our mappings:
modelMapper.validate();
If any TypeMap
contains a destination property that is unmatched to a source property a ValidationException
will be thrown.
Configuration
ModelMapper uses a set of conventions and configuration to determine which source and destination properties match each other. Available configuration, along with default values, is described below:
Setting | Description | Default Value |
---|---|---|
Ambiguity ignored | Determines whether destination properties that match more than one source property should be ignored | false |
Access level | Determines which methods and fields are eligible for matching based on accessibility | public |
Collections merge | Determines whether the destination items should be replaced or merged while source and destination have different size | true |
Field matching | Indicates whether fields are eligible for matching | disabled |
Naming convention | Determines which methods and fields are eligible for matching based on name | JavaBeans |
Full type matching | Determines wether ConditionalConverters must define a full match in order to be applied | false |
Implicit matching | Determines whether the implicit mapping (mapping the models intelligently) should be enabled (see Matching Process) | true |
Name transformer | Transforms eligible property and class names prior to tokenization | JavaBeans |
Name tokenizer | Tokenizes source and destination property names prior to matching | Camel Case |
Matching strategy | Determines how source and destination tokens are matched | Standard |
Prefer nested properties | Determines if the implicit mapping should map the nested properties, we strongly recommend to disable this option while you are mapping a model contains circular reference | true |
Skip null | Determines whether a property should be skipped or not when the property value is null | false |
You can read about how this configuration is used during the matching process.
Default Configuration
Default configuration uses the Standard matching strategy to match only public source and destination methods that are named according to the JavaBeans convention.
Configuration Examples
Adjusting configuration for certain matching requirements is simple. This example configures a ModelMapper
to allow protected
methods to be matched:
modelMapper.getConfiguration()
.setMethodAccessLevel(AccessLevel.PROTECTED);
This example configures a ModelMapper
to allow private fields to be matched:
modelMapper.getConfiguration()
.setFieldMatchingEnabled(true)
.setFieldAccessLevel(AccessLevel.PRIVATE);
This example configures a ModelMapper
to use the Loose
[MatchingStrategies matching strategy]:
modelMapper.getConfiguration()
.setMatchingStrategy(MatchingStrategies.LOOSE);
This example configures a ModelMapper
to allow any source and destination property names to be eligible for matching:
modelMapper.getConfiguration()
.setSourceNamingConvention(NamingConventions.NONE);
.setDestinationNamingConvention(NamingConventions.NONE);
This example configures a ModelMapper
to use the Underscore
name tokenizer for source and destination properties:
modelMapper.getConfiguration()
.setSourceNameTokenizer(NameTokenizers.UNDERSCORE)
.setDestinationNameTokenizer(NameTokenizers.UNDERSCORE)
Available Conventions
ModelMapper includes several pre-defined conventions for handling different property matching requirements:
Convention | Description |
---|---|
NamingConventions.NONE |
Represents no naming convention, which applies to all property names |
NamingConventions.JAVABEANS_ACCESSOR |
Finds eligible accessors according to JavaBeans convention |
NamingConventions.JAVABEANS_MUTATOR |
Finds eligible mutators according to JavaBeans convention |
NameTransformers.JAVABEANS_ACCESSOR |
Transforms accessor names according to JavaBeans convention |
NameTransformers.JAVABEANS_MUTATOR |
Transforms mutators names according to JavaBeans convention |
NameTokenizers.CAMEL_CASE |
Tokenizes property and class names according to Camel Case convention |
NameTokenizers.UNDERSCORE |
Tokenizes property and class names by underscores |
MatchingStrategies.STANDARD |
Intelligently matches source and destination properties |
MatchingStrategies.LOOSE |
Loosely matches source and destination properties |
MatchingStrategies.STRICT |
Strictly matches source and destination properties |
Matching Strategies
Matching strategies are used during the matching process to match source and destination properties to each other. Below is a description of each strategy.
Standard
The Standard matching strategy allows for source properties to be intelligently matched to destination properties, requiring that all destination properties be matched and all source property names have at least one token matched. The following rules apply:
- Tokens can be matched in any order
- All destination property name tokens must be matched
- All source property names must have at least one token matched
The standard matching strategy is configured by default, and while it is not exact, it is ideal to use in most scenarios.
Loose
The Loose matching strategy allows for source properties to be loosely matched to destination properties by requiring that only the last destination property in a hierarchy be matched. The following rules apply:
- Tokens can be matched in any order
- The last destination property name must have all tokens matched
- The last source property name must have at least one token matched
The loose matching strategy is ideal to use for source and destination object models with property hierarchies that are very dissimilar. It may result in a higher level of ambiguous matches being detected, but for well-known object models it can be a quick alternative to defining mappings.
Strict
The strict matching strategy allows for source properties to be strictly matched to destination properties. This strategy allows for complete matching accuracy, ensuring that no mismatches or ambiguity occurs. But it requires that property name tokens on the source and destination side match each other precisely. The following rules apply:
- Tokens are matched in strict order
- All destination property name tokens must be matched
- All source property names must have all tokens matched
The strict matching strategy is ideal to use when you want to ensure that no ambiguity or unexpected mapping occurs without having to inspect a TypeMap
. The drawback is that the strictness may result in some destination properties remaining unmatched.
API Overview
The ModelMapper API consists of a few principal types:
- ModelMapper
- The class you instantiate to perform object mapping, configure matching, load PropertyMaps and register Mappers
- Contains Configuration and TypeMaps
- PropertyMap
- The class you extend to define mappings between source and destination properties for a specific pair of types
- TypeMap
- The interface you use to perform configuration, introspection and mapping for a specific pair of types
- Contains property mappings that are added from a PropertyMap
- Created by a ModelMapper
- Converter
- The interface you implement to perform custom conversion between two types or property hierarchies
- Added to a ModelMapper, set against a TypeMap, or used in a mapping.
- Provider
- The interface you implement to provide instances of destination types.
- Set against a
TypeMap
or used in a mapping.
- Condition
- The interface you implement to conditionally create a mapping.
- Used in a mapping.
Also see the Property Mapping section of the User’s Guide for an overview of the Mapping API.
Integrations
ModelMapper was designed to be easily extensible via the API and SPI and to integrate well with existing technologies. Several integrations are described below.
General
Provider Integrations
Providers allow you to provide you own instance of destination objects prior to mapping. ModelMapper has several 3rd party integrations that allow for external libraries to provide destination objects:
Value Reader Integrations
Value Readers allow you to read and map values from different types of source object, aside from typical JavaBeans. ModelMapper has several 3rd party integrations that allow for the mapping of values from various types of source objects:
Native Integrations
Certain libraries natively integrate with ModelMapper without any additional dependencies. These include:
Spring Integration
ModelMapper’s Spring integration allows for the provisioning of destination objects to be delegated to a Spring BeanFactory during the mapping process.
Setup
To get started, add the modelmapper-spring
Maven dependency to your project:
<dependency>
<groupId>org.modelmapper.extensions</groupId>
<artifactId>modelmapper-spring</artifactId>
<version>3.0.0</version>
</dependency>
Usage
Let’s obtain a Spring integrated Provider, which will delegate to a BeanFactory whenever called:
Provider<?> springProvider = SpringIntegration.fromSpring(beanFactory);
Then we can configure the Provider for to be used globally for a ModelMapper:
modelMapper.getConfiguration().setProvider(springProvider);
Or set the Provider to be used for a specific TypeMap:
typeMap.setProvider(springProvider);
The provider can also be used for individual mappings:
with(springProvider).map().someSetter(source.someGetter());
Jackson Integration
ModelMapper’s Jackson integration allows you to map a Jackson JsonNode to a JavaBean.
Setup
To get started, add the modelmapper-jackson
Maven dependency to your project:
<dependency>
<groupId>org.modelmapper.extensions</groupId>
<artifactId>modelmapper-jackson</artifactId>
<version>3.0.0</version>
</dependency>
Next, configure ModelMapper to support the JsonNodeValueReader, which allows for values to be read and mapped from a JsonNode:
modelMapper.getConfiguration().addValueReader(new JsonNodeValueReader());
Usage Example
Consider the following JSON representing an order:
{
"id": 456,
"customer": {
"id": 789,
"street_address": "123 Main Street",
"address_city": "SF"
}
}
We may need to map this to a different object model:
// Assume getters and setters are present
public class Order {
private int id;
private Customer customer;
}
public class Customer {
private int id;
private Address address;
}
public class Address {
private String street;
private String city;
}
Since the order JSON in this example uses an underscore naming convention, we’ll need to configure ModelMapper to tokenize source property names by underscore:
modelMapper.getConfiguration().setSourceNameTokenizer(NameTokenizers.UNDERSCORE);
With that set, mapping a JsonNode for the order JSON to an Order object is simple:
JsonNode orderNode = new ObjectMapper().readTree(orderJson);
Order order = modelMapper.map(orderNode, Order.class);
And we can assert that values are mapped as expected:
assertEquals(order.getId(), 456);
assertEquals(order.getCustomer().getId(), 789);
assertEquals(order.getCustomer().getAddress().getStreet(), "123 Main Street");
assertEquals(order.getCustomer().getAddress().getCity(), "SF");
Explicit Mapping
While ModelMapper will do its best to implicitly match JsonNode values to destination properties, sometimes you may need to explicitly define how one property maps to another. A PropertyMap allows us to do this.
Let’s define how a JsonNode
maps to an Order
by creating a PropertyMap. Our PropertyMap will include a map()
statement that maps a source JsonNode’s customer.street_address
field hierarchy to a destination Order’s getCustomer().getAddress().setStreet()
method hierarchy:
PropertyMap<JsonNode, Order> orderMap = new PropertyMap<JsonNode Order>() {
protected void configure() {
map().getCustomer().getAddress().setStreet(this.<String>source("customer.street_address"));
}
};
To use our PropertyMap, we’ll create a TypeMap for our order JsonNode and add our PropertyMap to it:
modelMapper.createTypeMap(orderNode, Order.class).addMappings(orderMap)
We can then map JsonNodes to Orders as usual, with properties being mapped according to the PropertyMap that we defined:
Order order = modelMapper.map(orderNode, Order.class);
Things to Note
ModelMapper maintains a TypeMap for each source and destination type, containing the mappings between the two types. For “generic” types such as JsonNode this can be problematic since the structure of a JsonNode can vary. In order to distinguish structurally different JsonNodes that map to the same destination type, we can provide a type map name to ModelMapper.
Continuing with the example above, let’s map another order JSON, this one with a different structure, to the same Order class:
{
"id": 222,
"customer_id": 333,
"customer_street_address": "444 Main Street",
"customer_address_city": "LA"
}
Mapping this JSON to an order is simple, but we’ll need to provide a type map name to distinguish this JsonNode to Order mapping from the previous unnamed mapping:
JsonNode orderNode = new ObjectMapper().readTree(flatJson);
Order order = modelMapper.map(orderNode, Order.class, "flat");