Search code examples
fieldabstractmapstruct

MapStruct maps the model's field as empty when the field is of an abstract type


I have a class that has a field which in turn is an abstract class. I would like MapStruct to be able to map from the incoming DTO to the internal model including all the subfields of the abstract class, but I only get the empty object in return —rightfully so, since that's the way I've defined the mapping function—.

How can I make it so that MapStruct not only maps the top class, but also the field's classes? Do I have to create mappers for the Cat and Dog classes too, and then bring them in to the OwnerMapper class?

Thank you in advance!

Example setup

Mapper interface

@Mapper(componentModel = MappingConstants.ComponentModel.CDI)
public interface OwnerMapper {

    @Mapping(source = "pet", target = "pet", qualifiedByName = "mapPet")
    Owner toEntity(OwnerDTO owner);

    @Named("mapPet")
    default Pet mapPet(final PetDTO pet) {
        if (pet instanceof CatDTO) {
            return new Cat();
        } else {
            return new Dog();
        }
    }
}

Internal classes

public class Owner {
    private String name;
    private Pet pet;

    public Owner() { }
    // Omitting getters and setters.
}
public abstract class Pet {
    public abstract String makeNoise();
}
public final class Dog extends Pet {
    private UUID dogTag;
    private String name;

    public Dog() { }

    @Override
    public String makeNoise() {
        return "Bark!";
    }

    // Omitting getters and setters.
}
public final class Cat extends Pet {
    private boolean bell;

    public Cat() { }

    @Override
    public String makeNoise() {
        return "Meow!";
    }

    // Omitting getters and setters.

DTO classes

public class OwnerDTO {
    private String name;
    private PetDTO pet;

    public OwnerDTO() { }

    // Omitting getters and setters.
public abstract class PetDTO {
}
public final class CatDTO extends PetDTO {
    private UUID catTag;
    private boolean bell;

    public CatDTO() { }

    // Omitting getters and setters.
public final class DogDTO extends PetDTO {
    private UUID dogTag;

    private String name;

    public DogDTO() { }

    // Omitting getters and setters.

Solution

  • Well, I solved it by defining a few more mapping methods and calling them from the named mapper function:

    @Mapper(componentModel = MappingConstants.ComponentModel.CDI)
    public interface OwnerMapper {
    
        @Mapping(source = "pet", target = "pet", qualifiedByName = "mapPet")
        Owner toEntity(OwnerDTO owner);
    
        Cat catToEntity(CatDTO cat);
    
        Dog dogToEntity(DogDTO dog);
    
        @Named("mapPet")
        default Pet mapPet(final PetDTO pet) {
            if (pet instanceof CatDTO cat) {
                return this.catToEntity(cat);
            } else if (pet instanceof DogDTO dog) {
                return this.dogToEntity(dog);
            } else {
                throw new IllegalStateException("Unsupported pet type identified");
            }
        }
    }