Using Custom ModelMapper Converters and Mappings in a Spring Boot Application
In this post, I’m going to demonstrate using ModelMapper with custom property maps and converters to handle the transfer from data transfer object to entity and vice versa when some of the fields are represented by another class. This example has items and two of the items are represented by a Code class that is listed in a drop-down on the entry page. The example shows two types of custom mappings/converters: one for converting the string input from the form into a Code object on the entity class and the other converts the Code into a readable string when listing the items on the web page.
Prerequisites
- A basic understanding of Java and Spring Boot
- Java 8
- Preferred Java IDE – the example was created in Eclipse
- Internet connection
Gradle dependency
compile('com.github.jmnarloch:modelmapper-spring-boot-starter:1.1.0')
Introduction
To give a visual illustration of what we’re working with, here’s a simple ER Diagram to illustrate the entities used. The itemTypeCode and locationCode are both Code objects and will need to be mapped.
When allowing ModelMapper to use default mappings two problems occur:
- Adding a new Item results in nulls being stored for the two fields that refer to Codes
- Showing Items on the list that do have codes show non-user-friendly Java class information.
Problem two can be solved by overriding the toString method on the Code entity class. Using ModelMapper gives you the option of having one field displayed in one situation and another for a different situation. In the application that inspired this example, I map to Code.codeValue when going into edit mode so that it displays properly in the drop-down, but map to Code.shortDescription on my lists so that it’s nicer for the users.
Mapping an object on entry
I’ll address problem one first. In the controller method for saving a new Item, ModelMapper is called to map the ItemDTO instance coming from the request into an Item object that will be stored in the application. Without guidance, ModelMapper doesn’t know how to map the itemType and location from the DTO into a Code object in the itemTypeCode and locationCode fields on the entity object. As a result, the itemTypeCode and locationCode fields will be null.
Item item = modelMapper.map(itemDto, Item.class);
To properly convert the Code Value, we add both a custom org.modelmapper.Converter
and a custom org.modelmapper.PropertyMap
in the form of anonymous inner classes in our controller class. For the converters, the convert method is implemented. The codes are categorized by type, so Code.codeType plus Code.codeValue equals a unique Code. We use a custom converter for each code type. In this simple example, the codes are just stored in a static List in the main class, so there’s a loop for finding the appropriate Code. A real app would retrieve the Code from whatever datastore is being used.
/** * Converter for converting a string CodeValue on the DTO into a Code object with type ITEM_TYPE_CODE */ Converter<String, Code> itemTypeCodeConverter = new Converter<String, Code>() { public Code convert(MappingContext<String, Code> context) { Code itemTypeCode = null; for (Code code : ModelMapperExampleApplication.storedCodes) { if (code.getCodeType().equals(ModelMapperExampleApplication.ITEM_TYPE_CODE) && code.getCodeValue().equals(context.getSource())) { itemTypeCode = code; break; } } return itemTypeCode; } }; /** * Converter for converting a string CodeValue on the DTO into a Code object with type LOCATION_CODE */ Converter<String, Code> locationCodeConverter = new Converter<String, Code>() { public Code convert(MappingContext<String, Code> context) { Code locationCode = null; for (Code code : ModelMapperExampleApplication.storedCodes) { if (code.getCodeType().equals(ModelMapperExampleApplication.LOCATION_CODE) && code.getCodeValue().equals(context.getSource())) { locationCode = code; break; } } return locationCode; } };
Once the custom converters are in place, the custom PropertyMap implements the configure method to assign the appropriate custom converter to the proper field. This example indicates that ModelMapper should use the itemTypeConverter to map the itemType field to the itemTypeCode field and use locationCodeConverter to map location to locationCode.
/** * Mapping for converting String Code Keys from ItemDTO to Code objects. * To use: * modelMapper.addMappings(itemMap); * */ PropertyMap<ItemDTO, Item> itemMap = new PropertyMap<ItemDTO, Item>() { protected void configure() { using(itemTypeCodeConverter).map(source.getItemType()).setItemTypeCode(null); using(locationCodeConverter).map(source.getLocation()).setLocationCode(null); } };
The custom mapping has to be added to the mappings for the ModelMapper instance in this controller. I’d like this mapping to be used throughout the controller, so I set up a constructor for the controller and have an instance of ModelMapper injected. The line this.modelMapper.addMappings(itemMap);
adds the custom mapping to the modelMapper instance. Now when Model Mapper converts from the ItemDTO to an Item, the custom mapping will be used and Code objects will be properly placed in the itemTypeCode and locationCode fields.
@Autowired public ItemController(ModelMapper modelMapper) { this.modelMapper = modelMapper; this.modelMapper.addMappings(itemMap); }
Mapping from an Object for Viewing
In addressing problem two, the issue of displaying user-readable values on the list, we’ll give two options for how to translate the Codes. When going from the Code object to a string in the DTO, the Converters don’t need to be as specific. The converter just needs to retrieve a string from one of the code fields and put it in the DTO. The first converter below maps the Code to the Code.codeValue field. The second maps to the Code.shortDescription field.
/** * Converter for converting a into a String CodeValue on the DTO */ Converter<Code, String> codeToStringConverter = new Converter<Code, String>() { public String convert(MappingContext<Code, String> context) { return (context.getSource()!= null? context.getSource().getCodeValue():""); } }; /** * Converter for converting a into a String Short Description on the DTO */ Converter<Code, String> codeToStringDescConverter = new Converter<Code, String>() { public String convert(MappingContext<Code, String> context) { return (context.getSource()!= null? context.getSource().getShortDescription():""); } };
As with mapping a string to a code, we need to add a PropertyMap to tell ModelMapper how to use the custom converters. The itemDtoMap below configures the mapping to use the codeToString converter which will display the Code.codeValue field on the web page. As with the custom mapping for populating the entity with the right Code objects, we have to tell the ModelMapper instance to use the mapping, by adding the line this.modelMapper.addMappings(itemDtoMap);
to the constructor.
/** * Mapping for converting Code objects into String Code Keys on ItemDto. * To use: * modelMapper.addMappings(itemDtoMap); * */ PropertyMap<Item, ItemDTO> itemDtoMap = new PropertyMap<Item, ItemDTO>() { protected void configure() { using(codeToStringConverter).map(source.getItemTypeCode()).setItemType(null); using(codeToStringConverter).map(source.getLocationCode()).setLocation(null); } };
Here’s a screenshot of the codes that pre-load into the application.
This is what displays with the codeToStringConverter being applied to the itemDtoMap. The Type and Location fields now display Code.codeValue.
If we decide we’d rather show the shortDescription, we change itemDtoMap to use codeToStringDescConverter. Another use case might be to have a DTO specifically for displaying selected fields to a list rather than loading all the fields that might not actually be displayed.
/** * Mapping for converting Code objects into String Code Keys on ItemDto. * To use: * modelMapper.addMappings(itemDtoMap); * */ PropertyMap<Item, ItemDTO> itemDtoMap = new PropertyMap<Item, ItemDTO>() { protected void configure() { using(codeToStringDescConverter).map(source.getItemTypeCode()).setItemType(null); using(codeToStringDescConverter).map(source.getLocationCode()).setLocation(null); } };
Now the list of items displays the Code.shortDescription field for in the Type and Location column.
Conclusion
This example illustrates how to use custom ModelMapper Converters and Mappings in anonymous inner classes in a Spring Controller to map submitted string values to objects and map specific string fields in an object out to the browser.
The complete example code is available on GitHub at https://github.com/amdegregorio/ModelMapperExample
weird english language,
for example I cannot understand “Without guidance, ModelMapper, doesn’t know to map the codeValue string submitted in the itemType and location fields in the DTO into a Code object to put in the itemTypeCode and locationCode fields on the entity object. ”
???
never mind
LikeLike
Thanks for the feedback. That was definitely awkward. I’ve cleaned it up a bit and taken the opportunity to make another pass at my proofreading. Thanks again!
LikeLike