'Mapstruct: How to use custom mappers with @MappingTarget

I am using Mapstruct and I need to update an existing bean using @MappingTarget, but need to apply some complex logic to set the correct field in the target.

Lets say I have a target bean that looks like this. A user has a list of accounts, and one of those accounts is marked as favourite.

UserDetails {
  String name;
  List<Account> accounts;
}

Account {
  String id;
  boolean favourite;
}

The DTO class contains the account ID of their favourite account.

UserDetialsDTO {
  String name;
  String favouriteAccountId;
  List<String> accountIds;
}

I need to use some complex logic to update the correct Account in the list of accounts.

UserDetails fromDto(UserDetialsDTO dto, @MappingTarget UserDetails userDetails);

The logic of finding and updating the correct Account to make it favourite is something like this:

userDetails.accounts
           .stream()
           .forEach(acct -> acct.setFavourite(dto.favouriteAccountId.equals(acct.id))) ;

How can I tell Mapstruct to use this custom logic when updating a @MapingTarget?



Solution 1:[1]

try:

    @Mapper 
    public interface MyMapper {

         @Mapping( target = "accounts", ignore = true ) 
         void fromDto(UserDetialsDTO dto, @MappingTarget UserDetails userDetails);

         @AfterMapping
         default void handleAccounts(UserDetialsDTO dto, @MappingTarget UserDetails userDetails) {
             userDetails.accounts
               .stream()
               .forEach(acct -> acct.setFavourite(dto.favouriteAccountId.equals(acct.id))) ;
         }
    }

Solution 2:[2]

You can use a decorator, to implement both:

  • initialization of Account list
  • favorite custom logic

such as:

@Mapper
@DecoratedWith(MyMapperDecorator.class)
public interface MyMapper {
    void fromDto(UserDetialsDTO dto, @MappingTarget UserDetails userDetails);
}

public class MyMapperDecorator implements MyMapper{

    @Override
    public void fromDto(final UserDetialsDTO dto, final UserDetails userDetails) {
        if(dto == null){
            return;
        }

        dto.getAccountIds().forEach(a -> {
            final Account account = new Account();
            account.setId(a);
            account.setFavourite(a.equals(dto.getFavouriteAccountId()));
            userDetails.getAccounts().add(new Account());
        });

    }
}

In order to avoid duplicates I would suggest to use Set<Account> instead of List and implement Account.equals() accordingly.

An alternative (sligtly) more sophisticated approach could involve defining a String to Account mapper use it:

@Mapper(uses = AccountMapper.class)
@DecoratedWith(MyMapperDecorator.class)
interface MyMapper {

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

    @Mapping(target = "accounts", source="accountIds")
    abstract void update(UserDetialsDTO dto, @MappingTarget UserDetails userDetails);

}

public abstract class MyMapperDecorator implements MyMapper{

    private final MyMapper delegate;

    MyMapperDecorator(final MyMapper delegate){
        this.delegate = delegate;
    }

    @Override
    public void update(final UserDetialsDTO dto, final UserDetails userDetails) {
        if(dto == null){
            return;
        }

        delegate.update(dto, userDetails);

        userDetails.accounts
                .stream()
                .forEach(acct -> acct.setFavourite(dto.favouriteAccountId.equals(acct.id))) ;

    }
}

@Mapper
public interface AccountMapper {
    Account fromString(String id);

    void update(String id, @MappingTarget Account account);
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Sjaak
Solution 2