How to use ModelMapper with more complex objects in Spring Boot Java.

ModelMapper is a great library for object mapping in java. With simple objects there is no need to configure anything. You just have to ensure field names are the same and library will handle the rest. The issue arises with nested objects you have to flatten or project.


ModelMapper documentation provides two solutions to this problem. Either create PropertyMap or use Loose Matching strategy. The latter one is not ideal, as it can easily lead to ambiguities. When it comes to PropertyMaps, you have to add them to modelMapper before using them. In this tutorial we will create layer on top of modelMapper that will setup all necessary mappings for us.


DTO interface

Our solution requires use of DTO interface which has updateModelMapper method. We will be overriding this method whenever we need to add mappings.


public interface DTO {

    default ModelMapper updateModelMapper(ModelMapper mapper, MappingUtils utils){
        return mapper;
    }
}

Mapping Utils

This class is a service that will be responsible for mapping objects. For creating ModelMapper and adding mappings we have functions getMapper and updateMapping.


    public <T extends DTO> ModelMapper getMapper(Class<T> target){
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration()
                .setFieldAccessLevel(Configuration.AccessLevel.PRIVATE)
                .setFieldMatchingEnabled(true)
                .setPropertyCondition(context ->
                        !(context.getSource() instanceof PersistentCollection)
                );

        return updateMapping(modelMapper, target);
    }

    public  <T extends DTO> ModelMapper updateMapping(ModelMapper mapper, Class<T> dto){
        try {
            Constructor<T> constructor = dto.getConstructor();
            T instance = constructor.newInstance();
            return instance.updateModelMapper(mapper, this);
        }catch (NoSuchMethodException | InstantiationException | IllegalAccessException 
| InvocationTargetException e){
            throw new MappingException(dto.getName());
        }
    }

Both functions have Destination class that implements DTO interface as parameter. GetMapper creates ModelMapper instance, sets default configuration and calls updateMapping. UpdateMapping creates instance of destination class and calls updateModelMapper on it.


MappingUtils has also a couple of utility methods for easier mapping:


    public  <S, T extends DTO> List<T> mapList(List<S> source, Class<T> target){
        ModelMapper modelMapper = getMapper(target);

        return source
                .stream().map(el -> modelMapper.map(el, target))
                .collect(Collectors.toList());
    }

    public  T map(S source, Class<T> target){
        ModelMapper modelMapper = getMapper(target);

        return modelMapper.map(source, target);
    }

    public <S extends DTO, T> List<T> mapListFromDTO(List<S> source, Class<T> target){
        ModelMapper modelMapper = getMapper(source.get(0).getClass());

        return source
                .stream().map(el -> modelMapper.map(el, target))
                .collect(Collectors.toList());
    }
    public <S extends DTO, T> T mapFromDTO(S source, Class<T> target){
        ModelMapper modelMapper = getMapper(source.getClass());

        return modelMapper.map(source, target);
    }

Use case

To show how our MappingUtils work i have created three simple entities: City, Building and Room.


@Entity
@Data
public class City {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    private Long population;

    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "city")
    private Set buildings = new HashSet<>();
}

@Entity
@Data
public class Building {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    private String number;

    private String address;

    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "building")
    private Set rooms = new HashSet<>();

    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @ManyToOne
    private City city;

    public void updateRelations(){
        rooms.forEach(room -> room.setBuilding(this));
    }
}

@Data
@Entity
public class Room {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    private Integer number;

    private Integer floor;

    @ToString.Exclude
    @EqualsAndHashCode.Exclude
    @ManyToOne
    private Building building;
}

There will be two simple endpoints: one for downloading list of building and creating new building. Therefore we will need only BuildingRepository:


public interface BuildingRepository extends JpaRepository {

    @EntityGraph(attributePaths = {"rooms", "city"})
    List findAll();
}

Downloading list of buildings

Our building response will have a list of rooms so we will start from creating simple RoomDTO.


@Data
public class RoomDTO implements DTO {

    private Long id;

    private String name;

    private Integer number;

    private Integer floor;
}

After that, we can proceed to a more complex BuildingDTO:


@Data
public class BuildingDTO implements DTO {

    private Long id;

    private String name;

    private String number;

    private String address;

    private Integer roomNumber = 0;

    private List rooms = new ArrayList<>();

    private String city;

    @Override
    public ModelMapper updateModelMapper(ModelMapper mapper, MappingUtils utils) {
        mapper.addMappings(buildingMap(utils));

        return mapper;
    }

    public PropertyMap<Building, BuildingDTO> buildingMap(MappingUtils utils) {
        return new PropertyMap<Building, BuildingDTO>() {
            @Override
            protected void configure() {
                Converter<Building, Integer> getSize = new AbstractConverter<Building, Integer>() {
                    @Override
                    protected Integer convert(Building building) {
                        return building.getRooms().size();
                    }
                };

                Converter<List<Building>, List<RoomDTO>> mapRooms = new AbstractConverter<List<Building>, List<RoomDTO>>() {
                    @Override
                    protected List<RoomDTO> convert(Building building) {

                        return utils.mapList(new ArrayList<>(building.getRooms()), RoomDTO.class);
                    }
                };

                using(getSize).map(source, destination.getRoomNumber());
                using(mapRooms).map(source, destination.getRooms());
                map().setCity(source.getCity().getName());

            }
        };
    }
}

To create proper mapping we are overriding updateModelMapper from DTO interface. Inside it we are adding our custom PropertyMap. There we have three mappings and two converters. GetSize converter is required to get size of room set. MapRooms is using passed mappingUtils to map rooms to RoomDTO. Before we add nested mapping we have to be sure there is no loop! The mappings themself are straightforward. For simple mapping we are using map function. To apply converter we need to add using before map. Source represents Building and destination represents BuildingDTO.

And here is the controller:


    @GetMapping
    public List<BuildingDTO> getBuildings(){
        return mappingUtils
                .mapList(buildingRepository.findAll(), BuildingDTO.class);
    }

Creating new building

Again we will start from Room:


@Data
public class RoomCreator implements DTO {

    private String name;

    private Integer number;

    private Integer floor;
}

and then to BuildingCreator:


@Data
public class BuildingCreator implements DTO {

    private String name;

    private String number;

    private String address;

    private Long cityId;

    private List rooms;

    @Override
    public ModelMapper updateModelMapper(ModelMapper mapper, MappingUtils utils) {
        mapper.addMappings(buildingMap(utils));
        mapper.addMappings(cityMap());
        mapper.getConfiguration().setSkipNullEnabled(true);
        return mapper;
    }


    public PropertyMap<BuildingCreator, City> cityMap(){
        return new PropertyMap<BuildingCreator, City>() {
            @Override
            protected void configure() {
                map().setId(source.cityId);
            }
        };
    }

    public PropertyMap<BuildingCreator, Building> buildingMap(MappingUtils utils) {
        return new PropertyMap<BuildingCreator, Building>() {
            @Override
            protected void configure() {
                Converter<BuildingCreator, Set<Room>> mapRooms = new AbstractConverter<BuildingCreator, Set<Room>>() {
                    @Override
                    protected Set convert(BuildingCreator buildingCreator) {
                        return new HashSet<>(utils.mapListFromDTO(buildingCreator.getRooms(), Room.class));
                    }
                };

                using(mapRooms).map(source, destination.getRooms());
            }
        };
    }
}

This one is similar to BuildingDTO. For modelMapper we need to enable skipNull and add two PropertyMaps. One for mapping building and rooms and second for mapping city. When mapping rooms we need to remember to use proper function from mappingUtils.

And here is corresponding controller:


    @PostMapping
    public BuildingDTO createBuilding(@RequestBody BuildingCreator creator){
        Building building = mappingUtils.mapFromDTO(creator, Building.class);
        building.updateRelations();
        return mappingUtils
                .map(buildingRepository.save(building), BuildingDTO.class);
    }

We have to adjust relationships manually, otherwise building field from Room will remain null.

And that's all. You can use mappingUtils not only for mapping from entities to DTO, but also from DTO to entity.