John Ellis
John Ellis

Reputation: 213

mapstruct map iterable to non iterable

I want to map a list of Objects to an Object that contains a list:

public class Group {
    private List<Person> people;
}

public class Person {
    private String name;
}

I tried creating a mapper like this:

Group toGroup(List<Person> people);

and I'm getting this error:

error: Can't generate mapping method from iterable type to non-iterable type.

What is the most elegant solution for this kind of mapping?

Upvotes: 15

Views: 28728

Answers (3)

Evandro Oliveira
Evandro Oliveira

Reputation: 61

I would like to suggest a different approach if you are using a newer version of Java (14+) which is using a record to wrap your list of objects:

@Mapper
interface GroupMapper {

  record PeopleWrapper(List<People> people) {}

  default Group map(List<People> people) {
    return map(new PeopleWrapper(people));
  }

  Group map(PeopleWrapper wrapper);

}

This way you will be able to keep your original method call and isolate the wrapper object abstracting it away from your caller and making use of the succinct construct of the Java record, rather than a more verbose class definition and everything that comes with it.

Upvotes: 2

Nils Rommelfanger
Nils Rommelfanger

Reputation: 696

Mapstruct actually can do it. I have the exact same situation, a CollectionResponse that just contains items. I worked around it by adding a dummy parameter like this:

@Mapper(componentModel = "spring", uses = ItemMapper.class)
public interface CollectionResponseMapper {
   // Dummy property to prevent Mapstruct complaining "Can't generate mapping method from iterable type to non-iterable type."
   @Mapping( target = "items", source = "items")
   CollectionResponse map( Integer dummy, List<Item> items);
}

Mapstruct generates the desired code. Something like this:

public class CollectionResponseMapperImpl implements CollectionResponseMapper {

    @Autowired
    private ItemMapper itemMapper;

    @Override
    public CollectionResponse map(Integer dummy, List<Item> items) {
        if ( dummy == null && items == null ) {
            return null;
        }

        CollectionResponse collectionResponse = new CollectionResponse();

        if ( items != null ) {
            collectionResponse.setItems( itemListToItemDtoList( items ) );
        }

        return collectionResponse;
    }

    protected List<ItemDto> itemListToItemDtoList(List<Item> list) {
        if ( list == null ) {
            return null;
        }

        List<ItemDto> list1 = new ArrayList<ItemDto>( list.size() );
        for ( Item item : list ) {
            list1.add( itemMapper.mapItemToDto( item ) );
        }

        return list1;
    }
}

Upvotes: 16

Mykola Korol
Mykola Korol

Reputation: 743

General answer - Such mapping is prohibited.

To map a list of objects to an object that would wrap this list could be done by:

/// your class with business code
List<Person> people = ....
new Group(people);

/// group class
public class Group {
    private List<Person> people = new ArrayList<>();
    public Group(List<Person> people) {
       this.people = people
    }
}

When the Group would just have simply constructor with a list as param. You don't need to use Mapstruct for this. In the mapstruct sources have this check Mapstruct github sources for MethodRetrievalProcessor.java:

        Type parameterType = sourceParameters.get( 0 ).getType();

        if ( parameterType.isIterableOrStreamType() && !resultType.isIterableOrStreamType() ) {
            messager.printMessage( method, Message.RETRIEVAL_ITERABLE_TO_NON_ITERABLE );
            return false;
        }

So basically even Mapstruct team wants you to use mapping only when you need it. And doesn't want to allow transforming List<Object> to another Object as it doesn't make sense. This would make some sense if you're adding some additional information(non-iterable :) ) for your Group object, for example:

//group class
public class Group {
    private Long someCounter;
    private List<Person> people;
}

//mapper
@Mapping(target= "people", source ="people")
@Mapping(target= "someCounter", source ="counterValue")
Group toGroup(Long counterValue, List<Person> people);

But better use DTOs, Views, Entites and any other kind of objects that would hide all the nested stuff. In this case Mapstruct would be your greatest friend.

Upvotes: 8

Related Questions