Mapping Collections and Lists with MapStruct (List, Set, Map, Streams)

Mapping collections—such as List, Set, and Map—between domain entities and Data Transfer Objects (DTOs) is a routine task in Java applications. MapStruct simplifies this process by automatically generating null-safe, highly optimized loop code at compile time. In this comprehensive guide, we will explore how MapStruct handles collection conversions, how to customize element-level mapping with @IterableMapping and @MapMapping, how to handle null vs. empty collections, and how to update existing collection targets cleanly.


1. How Collection Mapping Works in MapStruct

MapStruct handles collection mappings seamlessly. If you define a mapping method for individual objects (e.g., User to UserDto), MapStruct automatically uses that method to generate iteration code for collection types like List<User> to List<UserDto> or Set<User> to Set<UserDto>.

You simply declare the collection mapping signature in your @Mapper interface:

import org.mapstruct.Mapper;
import java.util.List;
import java.util.Set;

@Mapper(componentModel = "cdi")
public interface UserMapper {

    // Single object mapping
    UserDto toDto(User user);

    // Collection mappings (MapStruct generates element-by-element loops automatically)
    List<UserDto> toDtoList(List<User> users);

    Set<UserDto> toDtoSet(Set<User> users);
}

Under the hood, MapStruct generates plain Java code that checks for null input, initializes an ArrayList or HashSet, iterates through the source collection, transforms each element using toDto(user), and returns the result.


2. Customizing Element-Level Mappings with @IterableMapping

When mapping a list, you might need to apply specific formatting, selection rules, or custom qualifier methods to every element in that list. MapStruct provides the @IterableMapping annotation for this exact purpose.

Example: Formatting Dates or Using Qualified Methods

Suppose you want to convert a list of Date objects into formatted String dates or invoke a specific named mapping method for list elements:

import org.mapstruct.IterableMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Named;
import java.util.List;

@Mapper(componentModel = "cdi")
public interface OrderMapper {

    @Named("shortTitle")
    default String toShortTitle(String title) {
        if (title == null) return null;
        return title.length() > 10 ? title.substring(0, 10) + "..." : title;
    }

    // Apply specific qualifier to each element in the list
    @IterableMapping(qualifiedByName = "shortTitle")
    List<String> toShortTitleList(List<String> titles);
}
Note: You cannot use @Mapping on a collection mapping method directly. @Mapping applies to object properties, whereas @IterableMapping applies to collection elements.

3. Mapping Maps (Key-Value Pairs) with @MapMapping

Mapping Java Map<K, V> structures requires transforming both the keys and the values. MapStruct provides the @MapMapping annotation to control formatting, qualifiers, and target types for keys and values independently.

import org.mapstruct.MapMapping;
import org.mapstruct.Mapper;
import java.util.Date;
import java.util.Map;

@Mapper(componentModel = "cdi")
public interface InventoryMapper {

    @MapMapping(
        keyDateFormat = "yyyy-MM-dd",
        valueNumberFormat = "$#.00"
    )
    Map<String, String> toFormattedMap(Map<Date, Double> rawMap);
}

You can also use keyQualifiedByName and valueQualifiedByName to specify custom methods for keys or values individually:

@MapMapping(
    keyQualifiedByName = "stringToEnumKey",
    valueQualifiedByName = "entityToDtoValue"
)
Map<StatusEnum, UserDto> toStatusUserMap(Map<String, UserEntity> map);

4. Handling Null vs. Empty Collections

By default, if the source collection passed to a mapping method is null, MapStruct returns null. However, in many enterprise applications, returning an empty collection (e.g., Collections.emptyList()) is preferred to avoid NullPointerException downstream.

Configuring Null Iterable Strategy

You can change this behavior at the mapper level or globally using nullValueIterableMappingStrategy:

import org.mapstruct.Mapper;
import org.mapstruct.NullValueIterableMappingStrategy;

@Mapper(
    componentModel = "cdi",
    nullValueIterableMappingStrategy = NullValueIterableMappingStrategy.RETURN_DEFAULT
)
public interface CustomerMapper {

    // If source 'customers' is null, returns an empty ArrayList instead of null
    List<CustomerDto> toDtoList(List<Customer> customers);
}

Available strategies for NullValueIterableMappingStrategy:

  • RETURN_NULL (Default): Returns null if the input collection is null.
  • RETURN_DEFAULT: Returns an empty collection (e.g., empty ArrayList, HashSet, or HashMap) if the input is null.

5. Updating Existing Target Collections (@MappingTarget)

When updating existing JPA entities or domain models, you often want to update a collection in-place rather than replacing the collection instance entirely (which can break ORM/Hibernate dirty checking and cascade operations).

You can control how MapStruct updates collections using CollectionMappingStrategy:

import org.mapstruct.CollectionMappingStrategy;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import java.util.List;

@Mapper(
    componentModel = "cdi",
    collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED
)
public interface CompanyMapper {

    void updateCompanyFromDto(CompanyDto dto, @MappingTarget Company entity);
}

Collection Mapping Strategies Explained:

Strategy Behavior
ACCESSOR_ONLY (Default) Clears or sets the target collection directly using getter/setter (e.g., target.setItems(...)).
SETTER_PREFERRED Uses setter if available; falls back to getter and modifying collection elements directly.
ADDER_PREFERRED Uses addXxx() / removeXxx() methods if present on the target class (ideal for JPA parent-child relationships).

6. Mapping Java Streams

MapStruct natively supports mapping Java Stream objects to collections or other streams. This is particularly useful when working with reactive pipelines or database queries returning streams.

import org.mapstruct.Mapper;
import java.util.List;
import java.util.stream.Stream;

@Mapper(componentModel = "cdi")
public interface StreamMapper {

    UserDto toDto(User user);

    // Map Stream to List
    List<UserDto> toDtoList(Stream<User> userStream);

    // Map Stream to Stream
    Stream<UserDto> toDtoStream(Stream<User> userStream);
}
Caution with Streams: When mapping Stream to Stream, the returned stream should be closed properly (e.g., using a try-with-resources block) if the underlying source stream backed an active I/O or database session.

7. Quick Reference Matrix

Requirement Annotation / Strategy Example Usage
Customize List element mapping @IterableMapping @IterableMapping(qualifiedByName = "myConverter")
Format Map keys or values @MapMapping @MapMapping(keyDateFormat = "yyyy-MM-dd")
Return empty list instead of null NullValueIterableMappingStrategy RETURN_DEFAULT
Use JPA addItem() methods CollectionMappingStrategy ADDER_PREFERRED

8. Frequently Asked Questions (FAQ)

Can MapStruct map immutable collections like List.of() or Guava ImmutableList?

Yes. If your target type or constructor expects an immutable collection, MapStruct detects the immutable target or constructor signature and passes the generated array/collection to the appropriate constructor or factory method.

Why do I get a compilation error when using @Mapping on a List mapping method?

The @Mapping annotation is designed to map named properties of complex objects (e.g., dto.firstName to entity.firstName). For collections, use @IterableMapping to configure how elements inside the list are processed.

How does MapStruct handle Set implementations?

MapStruct instantiates a HashSet by default when mapping to a Set target type. If your target interface is SortedSet, MapStruct will instantiate a TreeSet automatically.


Conclusion

Mapping collections in MapStruct requires minimal boilerplate while offering fine-grained control over element conversion, null safety, and target collection updates. By leveraging @IterableMapping, @MapMapping, and CollectionMappingStrategy, you can keep your Java DTO mapping layer performant, type-safe, and clean.


Recommended Articles

Mastering Enum Mapping in Enterprise Java Applications: From WildFly 29 to DTOs

Learn how to map Java Enums across database entities and external API DTOs with MapStruct, including @ValueMapping for explicit translations.

Mastering MapStruct Compilation Errors in Enterprise Java Applications

Learn how to resolve common MapStruct compile-time errors and ensure your Maven/Gradle builds succeed. #MapStruct #Java #EnterpriseJava #CompileTimeErrors

Advanced MapStruct Features: Mapping from Multiple Sources and Using Java Expressions

Advanced MapStruct 1.6.3 tutorial: mapping from multiple sources, Java expressions, @AfterMapping, nested mappings, custom methods, and common compile-time pitfalls.

Architecting MapStruct with Spring Boot 3: Complete Integration Guide

Master MapStruct with Spring Boot 3. Build a type-safe, compile-time mapping pipeline using Spring 6, dependency injection, and the Conversion Service.