Search code examples
javaspringspring-data-jpamapstruct

MapStruct mapper not mapping nested DTOs properly in spring boot


I have a DTO that doesn't work with mapping

    @Data
public class IndividualDTO {
    private String passportNumber;
    private String phoneNumber;
    private UserDTO user;

    @Data
    public static class UserDTO {

        @NotBlank
        @Email
        private String email;
        private String secretKey;
        private String firstName;
        private String lastName;
        private AddressDTO address;
    }

    @Data
    public static class AddressDTO {
        private String address;
        private String city;
        private String state;
        private String zipCode;
        private CountryDTO country;
    }

    @Data
    public static class CountryDTO{
        private String name;
    }
}

I am trying to map using MapStruct in this way

IndividualMapper

@Mapper(componentModel = "spring", uses = {UserMapper.class, AddressMapper.class, CountryMapper.class})
public interface IndividualMapper {

    IndividualMapper INSTANCE = Mappers.getMapper(IndividualMapper.class);

    Individual toEntity(IndividualDTO dto);

    IndividualDTO toDto(Individual entity);
}

AddressMapper

@Mapper(componentModel = "spring")
public interface AddressMapper {

    Address toAddressEntity(IndividualDTO.AddressDTO addressDTO);

    IndividualDTO.AddressDTO toAddressDto(Address address);
}

CountryMapper

@Mapper(componentModel = "spring")
public interface CountryMapper {

    Country toCountryEntity(IndividualDTO.CountryDTO countryDTO);

    IndividualDTO.CountryDTO toCountryDto(Country country);
}

UserMapper

@Mapper(componentModel = "spring")
public interface UserMapper {
    
    User toUserEntity(IndividualDTO.UserDTO userDTO);

    IndividualDTO.UserDTO toUserDto(User user);
}

As a result, I get:

@Component
public class IndividualMapperImpl implements IndividualMapper {

    @Override
    public Individual toEntity(IndividualDTO dto) {
        if ( dto == null ) {
            return null;
        }

        Individual individual = new Individual();

        return individual;
    }

    @Override
    public IndividualDTO toDto(Individual entity) {
        if ( entity == null ) {
            return null;
        }

        IndividualDTO individualDTO = new IndividualDTO();

        return individualDTO;
    }
}

According to the MapStruct documentation, everything should work, but unfortunately I ran into this problem.

MapStruct generated the IndividualMapperImpl class for me, which has no logic


Solution

  • According to the mapstruct FAQ, to use Lombok (1.18.16 version or newer) and Mapstruct together, you have to add lombok-binding path to the config of the maven-compiler plugin:

    <build>
            <pluginManagement>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-compiler-plugin</artifactId>
                        <version>${maven-compiler-plugin.version}</version>
                        <configuration>
                            <source>${java.version}</source>
                            <target>${java.version}</target>
                            <annotationProcessorPaths>
                                <path>
                                    <groupId>org.mapstruct</groupId>
                                    <artifactId>mapstruct-processor</artifactId>
                                    <version>${org.mapstruct.version}</version>
                                </path>
                                <path>
                                    <groupId>org.projectlombok</groupId>
                                    <artifactId>lombok</artifactId>
                                    <version>${org.projectlombok.version}</version>
                                </path>
                                <path>
                                    <groupId>org.projectlombok</groupId>
                                    <artifactId>lombok-mapstruct-binding</artifactId>
                                    <version>${lombok-mapstruct-binding.version}</version>
                                </path>
                            </annotationProcessorPaths>
                        </configuration>
                    </plugin>
                </plugins>
            </pluginManagement>
        </build>
    

    So please check pom file to make sure it is configured correctly according to the Mapstruct doc.

    Additional comments:

    1. You don't need to declare an INSTANCE variable in the mapper (like you did in IndividualMapper), since you're using componentModel = "spring". You can simple autowire your mapper into other spring beans:
    @Service
    public class SomeService {
        
        @Autowired
        private IndividualMapper individualMapper;
    }
    
    1. You declared the uses attribute of the @Mapper annotation only for IndividualMapper with all mappers (even though it only needs to use the UserMapper), and did not tell the other mappers which mappers they should use. This results in UserMapper not using AddressMapper, as intended (Check this in the generated mapperImpl-s). So add the uses attribute with the appropriate values ​​to the relevant mappers:
    @Mapper(componentModel = "spring", uses = UserMapper.class)
    public interface IndividualMapper {
        
        Individual toEntity(IndividualDTO dto);
    
        IndividualDTO toDto(Individual entity);
    }
    
    @Mapper(componentModel = "spring", uses = AddressMapper.class)
    public interface UserMapper {
    
        Individual.User toUserEntity(IndividualDTO.UserDTO userDTO);
    
        IndividualDTO.UserDTO toUserDto(Individual.User user);
    }
    
    // and so on for other mappers