11

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?

John Ellis
  • 173
  • 2
  • 2
  • 7
  • 1
    Possible duplicate of [mapping of non-iterable to iterable in mapstruct](https://stackoverflow.com/questions/47050419/mapping-of-non-iterable-to-iterable-in-mapstruct) – Sjaak May 12 '19 at 13:09

3 Answers3

11

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;
    }
}
8

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.

Mykola Korol
  • 638
  • 6
  • 10
  • This method that you give `Group toGroup(Long counterValue, List people);` is not working for me. Can't generate from iterable to non-iterable. – Ramanujan R Feb 23 '21 at 05:53
  • 2
    @RamanujanR please check the ordering of your parameters. You can go with a non-iterable target only when you have a non-iterable first parameter and vice-versa. Also - please pay attention that in my example the `List` is a field within the `Group`. And it won't work if you'll try to pass `List` in the input, but expect to have single `Person` as the field of `Group` - for that you need specify how do you map iterable to non-iterable (for example placing default method within the mapper class with desired signature and actual mapping inside) – Mykola Korol Feb 24 '21 at 14:06
  • You are right @mykola-karol. This works: `Group toGroup(Long counterValue, List people);`. This won't `Group toGroup(List people, Long counterValue);`. Ordering matters and I wonder why MapStruct has restricted the first ordering and not the second!! – Ramanujan R Mar 02 '21 at 04:07
  • @RamanujanR, great! :) Now you can upvote my post :D P.S. I have posted the code from `MethodRetrievalProcessor.java` in the Mapstruct. They simply get the first item, and I suppose they do that because if they'll try to do that dynamically - that would cause hard-to-track/unexpected behaviour, so they are keeping it as simple as possible (not to mention it actually builds a relation-graph from all the objects and sometimes can do 5-6layers of mapping between the objects you didn't expect it to do :D ) – Mykola Korol Mar 03 '21 at 10:26
0

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.