hamed
hamed

Reputation: 8033

java mapstruct - mapping a field inside related collection

I want to use mapstruct library for mapping models list to dto list in my spring application. Suppose I have two models something like this:

public class Employee {
    private Integer id;
    private String name;
    private Set<Phone> phones;
}

public class Phone {
    private Integer id;
    private String number;
}

And here is my two dtos:

public class EmployeeDto {
    private Integer id;
    private String name;
    private Set<PhoneDto> phones;
}

public class PhoneDto {
    private Integer id;
    private String num;
}

And finally I'm using this method in my mapping class:

@Mappings({
        @Mapping(target = "num", source = "phones.number")
})
public abstract List<EmployeeDto> toEmployeeDtoList(List<Employee> employeeList);

But this returns me java: No property named "phones.number" exists in source parameter(s). when I want to compile. I know something is wrong with my code, but I can't find something useful for my need. Can you please help me for solving this problem?

Upvotes: 3

Views: 21952

Answers (3)

  • Your Phone class has no mapper.
  • Your phone number field has different names between entity and dto
  • You should apply the Phone mapper mechanism to your parent interface Employee

PhoneMapper.class:

@Mapper
public interface PhoneMapper  {
    @Mapping(target = "num", source = "number")
    PhoneDto toDto(Phone phone);
}

EmployeeMapper.class

@Mapper(uses = PhoneMapper.class)
public interface EmployeeMapper {

    List<EmployeeDto> toDto(List<Employee> e);
}

That should work

Upvotes: 0

Mykola Korol
Mykola Korol

Reputation: 743

First reason: you should specify object -> object mapping before you can specify collection -> collection mapping(PhoneDto -> Phone, EmployeeDto -> Employee) as mapstruct nesting notation does not extend into collections. And from my perspective you don't need to hold basic collection mappings within the mapper. You always can do:

employees.stream()
        .map(mapper::toDto)
        .collect(Collectors.toList());

Note: But if you need some specific collection -> collection mapping on nested collection, you should specify it. (in your case Set might be ordered using LinkedHashSet underneath, and if you don't specify collection -> collection mapping, you would lose ordering, because mapstruct would use HashSet as default implementation for Set<Phones> -> Set<PhonesDto> transformation).

Mapstruct would pick all the mapping chain if the mapping is accessible for the mapper (the nested class mappers should be in the same class or would be stated in @Mapper(uses= class annotation).


Second reason: Yours @Mapping(target = "num", source = "phones.number") <<-- won't work because it doesn't know from what element from phones collection the number should be retrieved. It's like you're trying to write EmployeeDto.num(single record) = Emloyee.phones(multiple records).number(single record).


IMHO block: Best practice for using mapstruct is using clean interfaces. That shows that you have clear and transparent structure and good relations within your entity/dto/view/model/etc. If there would be need for something more concrete - you can always specify default method with @AfterMapping or @BeforeMapping annotation. Or go to abstract class implementation/decorators (@DecoratedWith mapping).

There is some dirty hack for such cases - @Mapping(target = "num", expression = "java(your_java_code_as_string_in_here)") but be aware: that expression is a string, and will fail only on mappers creation and won't work in all kinds of refactoring.


This is example mapping for your models (in both ways):

@Mapper
public interface EmployeeMapper {

  Employee toEmployee(EmployeeDto employeeDto);

  EmployeeDto toEmployeeDto(Employee employee);

  @Mapping(target="number", source="num")
  Phone toPhone(PhoneDto phoneDto);

  @InheritInverseConfiguration
  PhoneDto toPhoneDto(Phone phone);

  List<EmployeeDto> toEmployeeDtoList(List<Employee> employeeList);
}

Also good practice to consider - different mappers for each logic object pair.

@Mapper(uses = {PhoneMapper.class, OtherMapper.class}) // this is class level annotation.

The great examples are gathered here: https://github.com/mapstruct/mapstruct-examples/

Upvotes: 7

riccio
riccio

Reputation: 87

I found your answer located here https://www.baeldung.com/mapstruct

I believe that your issue is that you dont actually want the value phones.number to be mapped to num. You want the value number from the Phone class to map to the value num from the PhoneDto class.

@Mappings({
        @Mapping(target = "num", source = "number")
})
public abstract List<EmployeeDto> toEmployeeDtoList(List<Employee> employeeList);

Upvotes: -2

Related Questions