MapStruct does not copy subclass properties

Viewed 1915

I have the below structure. The properties from my subclass are not getting copied to my DTO.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
class BaseClass
{
 private Integer baseProperty1;
 private Integer baseProperty2;
 //getters & setters
}


class SubClass extends BaseClass
{
 private Integer subProperty1;
 private Integer subProperty2;
 //getters & setters
}

class BaseSubDTO
{
 private Integer baseProperty1;
 private Integer baseProperty2;
 private Integer subProperty1;
 private Integer subProperty2;
 //getters & setters
}

class BaseClassService
{
 public BaseClass find()
 {
  return baseClassRepository.findById(101);
 }
}

class BaseClassController
{
 public BaseSubDTO find()
 {
  return mapper.toDTO(baseClassService.find());
 }
}

@Mapper(componentModel = "spring")
public interface DTOMapper
{
    BaseSubDTO toDTO(final BaseClass entity);
}

The line:

return mapper.toDTO(baseClassService.find());

in the controller does not map the subclass properties subProperty1, subProperty2 to my BaseSubDTO.

How can the subclass properties be mapped into the DTO?

2 Answers

It seems that Downcast Mapping is not supported yet in mapstruct. See Support for Type-Refinement mapping (or Downcast Mapping)

In order to keep things generic, you could implement a custom mapper that checks the type of the passed in object:

@Mapper
public interface ToDTOMapper {
    ToDTOMapper MAPPER = Mappers.getMapper(ToDTOMapper.class);

    BaseSubDTO toDTOFromBaseClass(BaseClass baseClass);

    BaseSubDTO toDTOFromSubClass(SubClass baseClass);

    default BaseSubDTO map(BaseClass baseClass) {
        if(baseClass instanceof SubClass) {
            return toDTOFromSubClass((SubClass)baseClass);
        } 
        return toDTOFromBaseClass(baseClass);
    }
}

usage:

SubClass subClass = ...
BaseClass baseClass = ...

ToDTOMapper mapper = ToDTOMapper.MAPPER;
BaseSubDTO dto = mapper.map(subClass);
dto = mapper.map(baseClass);

Hope that helps.

The issue here is that the actual types of the mapper method are used to determine the mapping; subtypes are not taken into consideration. Namely, only the properties on the exact input type (BaseClass) are mapped to BaseSubDTO, not any fields that might be on a subtype (e.g. SubClass).

This makes extra sense when you realize that mapping is done at compilation time based on the types of the class, not at runtime based on reflection. In particular, note that there is no way to know at compile time what possible subtypes exist or will exist for a given superclass, so it's impossible for MapStruct to provide this functionality at compile time.

That being said, the upcoming MapStruct 1.5.0 provides a @SubclassMapping annotation which allows specifying known subclasses to be handled by the mapping method. If a known subtype is passed in, a more specific mapping method will be delegated to.

This can be applied to your particular example as follows:

@Mapper(componentModel = "spring")
public interface DTOMapper {
    @SubclassMapping(source = SubClass.class, target = BaseSubDTO.class)
    @Mapping(target = "subProperty1", ignore = true)
    @Mapping(target = "subProperty2", ignore = true)
    BaseSubDTO toDTO(final BaseClass entity);

    BaseSubDTO toSubclassDTO(final SubClass entity);
}
Related