How to Map Nested Properties Using MapStruct
This article explores how to perform nested mapping using the MapStruct library. Nested mapping is a common requirement in enterprise applications where data models are composed of complex, hierarchical structures.
MapStruct is a Java annotation processor that generates type-safe, high-performance mapping code at compile-time. It significantly reduces boilerplate code required for object mapping, making it ideal for converting between domain models and Data Transfer Objects (DTOs).
This article demonstrates how to automatically handle collections of nested objects and implement bi-directional mapping for conversion between entities and DTOs.
1. Use Case Overview
Imagine an application that manages music libraries. It has the following model structure:
- A
Librarycontains a list ofSongobjects. - Each
Songincludes aTrackobject. - The
Trackincludes details likeartistName,albumName, andduration.
We want to map this into a flat DTO structure where:
SongDTOincludestitle,album,artist, and a nestedTrackDTOforduration.
2. Maven Configuration
Below is the Maven configuration that includes the MapStruct library and the annotation processor plugin for compilation.
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.6.3</version>
</dependency>
</dependencies>
<build>
<finalName>mapstruct-nested-mapping</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.6.3</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
This configuration includes the MapStruct dependency. The annotation processor is essential for MapStruct to generate the implementation classes during compilation.
3. Source Entity Classes
Let’s define the domain model classes: Library, Song, and Track. These will represent the core domain logic in the application.
public class Library {
private String name;
private List<Song> songs;
}
public class Song {
private String title;
private Track track;
}
public class Track {
private String artistName;
private String albumName;
private int duration; // duration in seconds
}
These classes define a Library containing multiple Song instances. Each Song is linked to a Track, which contains metadata.
4. Target DTO Classes
Next, we define the target Data Transfer Objects that we want to map to.
public class TrackDTO {
private int duration;
}
TrackDTO represents a simplified version of the Track entity, exposing only the duration field.
public class SongDTO {
private String title;
private String artist;
private String album;
private TrackDTO track;
}
SongDTO provides a flattened view of the Song entity by directly exposing the artist and album fields that are originally nested within Track. It also retains a nested TrackDTO for duration, demonstrating how MapStruct can handle both flattened and nested mappings in the same class.
public class LibraryDTO {
private String name;
private List<SongDTO> songs;
}
LibraryDTO serves as the top-level DTO containing a collection of SongDTO objects. It mirrors the Library entity structure.
5. Mapper Interface with Nested Mappings
The @Mapping annotation is essential for handling cases where field names or structures differ between the source and target objects. It explicitly tells MapStruct how to map specific fields, including mapping from nested properties (e.g., track.artistName) to flattened DTO fields (e.g., artist).
Below is the mapper interface that defines how the Library, Song, and Track entities are converted into their corresponding DTOs, and vice versa.
@Mapper
public interface LibraryMapper {
LibraryMapper INSTANCE = Mappers.getMapper(LibraryMapper.class);
// Flatten nested Track to SongDTO
@Mapping(source = "track.artistName", target = "artist")
@Mapping(source = "track.albumName", target = "album")
SongDTO songToDto(Song song);
// Track to TrackDTO
TrackDTO trackToDto(Track track);
// Library to LibraryDTO (includes List<Song> to List<SongDTO>)
LibraryDTO libraryToDto(Library library);
// Reverse mappings
@Mapping(target = "track.artistName", source = "artist")
@Mapping(target = "track.albumName", source = "album")
@Mapping(target = "track.duration", source = "track.duration")
Song songFromDto(SongDTO dto);
Track trackFromDto(TrackDTO dto);
Library libraryFromDto(LibraryDTO dto);
}
The @Mapping annotation is used to define explicit field-to-field mappings when the source and target field names or structures differ. For example, @Mapping(source = "track.artistName", target = "artist") maps a nested property (artistName inside Track) to a flat property (artist) in SongDTO.
Similarly, the reverse mappings use @Mapping(target = "track.artistName", source = "artist") to reconstruct the nested Track structure from the flattened DTO. The libraryToDto() and libraryFromDto() methods automatically handle collections by recursively applying the appropriate mapping logic to the List<Song> and List<SongDTO> types.
Finally, when the project is built, the MapStruct processor enabled through the Maven plugin automatically generates the LibraryMapperImpl class during compilation. This class contains the actual mapping logic based on the definitions provided in the LibraryMapper interface. Below is a truncated version of the generated LibraryMapperImpl class:
public class LibraryMapperImpl implements LibraryMapper {
@Override
public SongDTO songToDto(Song song) {
if ( song == null ) {
return null;
}
SongDTO songDTO = new SongDTO();
songDTO.setArtist( songTrackArtistName( song ) );
songDTO.setAlbum( songTrackAlbumName( song ) );
songDTO.setTitle( song.getTitle() );
songDTO.setTrack( trackToDto( song.getTrack() ) );
return songDTO;
}
@Override
public TrackDTO trackToDto(Track track) {
if ( track == null ) {
return null;
}
TrackDTO trackDTO = new TrackDTO();
trackDTO.setDuration( track.getDuration() );
return trackDTO;
}
@Override
public LibraryDTO libraryToDto(Library library) {
if ( library == null ) {
return null;
}
LibraryDTO libraryDTO = new LibraryDTO();
libraryDTO.setName( library.getName() );
libraryDTO.setSongs( songListToSongDTOList( library.getSongs() ) );
return libraryDTO;
}
@Override
public Song songFromDto(SongDTO dto) {
if ( dto == null ) {
return null;
}
Song song = new Song();
if ( dto.getTrack() != null ) {
if ( song.getTrack() == null ) {
song.setTrack( new Track() );
}
trackDTOToTrack( dto.getTrack(), song.getTrack() );
}
if ( song.getTrack() == null ) {
song.setTrack( new Track() );
}
songDTOToTrack( dto, song.getTrack() );
song.setTitle( dto.getTitle() );
return song;
}
@Override
public Track trackFromDto(TrackDTO dto) {
if ( dto == null ) {
return null;
}
Track track = new Track();
track.setDuration( dto.getDuration() );
return track;
}
// ...additional methods
}
This class demonstrates how MapStruct compiles the annotated mappings into fully working Java code. Each @Mapping declared in the interface becomes a field assignment here. Note how MapStruct checks for null to avoid NullPointerException, and creates new instances of nested DTOs or entities before assigning values.
6. Example Usage
Here’s a Main class to run and see the mapper in action.
public class Main {
public static void main(String[] args) {
// Create nested Track
Track track = new Track();
track.setArtistName("Pink Floyd");
track.setAlbumName("The Wall");
track.setDuration(385);
// Create Song with nested Track
Song song = new Song();
song.setTitle("Another Brick in the Wall");
song.setTrack(track);
// Create Library with nested Song
Library library = new Library();
library.setName("Classic Rock");
List<Song> songs = new ArrayList<>();
songs.add(song);
library.setSongs(songs);
// Map Library to LibraryDTO
LibraryDTO libraryDTO = LibraryMapper.INSTANCE.libraryToDto(library);
System.out.println(" Mapped to DTO:");
System.out.println("LibraryDTO Name: " + libraryDTO.getName());
SongDTO mappedSongDTO = libraryDTO.getSongs().get(0);
System.out.println(" SongDTO Title: " + mappedSongDTO.getTitle());
System.out.println(" Artist (flattened): " + mappedSongDTO.getArtist());
System.out.println(" Album (flattened): " + mappedSongDTO.getAlbum());
System.out.println(" Duration (nested TrackDTO): " + mappedSongDTO.getTrack().getDuration());
// Map DTO back to Library entity
Library mappedBackLibrary = LibraryMapper.INSTANCE.libraryFromDto(libraryDTO);
System.out.println("\n Mapped Back to Entity:");
System.out.println("Library Name: " + mappedBackLibrary.getName());
Song mappedSong = mappedBackLibrary.getSongs().get(0);
System.out.println(" Song Title: " + mappedSong.getTitle());
Track mappedTrack = mappedSong.getTrack();
System.out.println(" Artist Name: " + mappedTrack.getArtistName());
System.out.println(" Album Name: " + mappedTrack.getAlbumName());
System.out.println(" Duration: " + mappedTrack.getDuration());
}
}
Expected Console Output
Mapped to DTO: LibraryDTO Name: Classic Rock SongDTO Title: Another Brick in the Wall Artist (flattened): Pink Floyd Album (flattened): The Wall Duration (nested TrackDTO): 385 Mapped Back to Entity: Library Name: Classic Rock Song Title: Another Brick in the Wall Artist Name: Pink Floyd Album Name: The Wall Duration: 385
This output confirms:
Track.artistNamewas mapped toSongDTO.artistand then back again.Track.albumNamewas mapped toSongDTO.albumand then back again.Track.durationwas preserved through nested mapping viaTrackDTO.
7. Conclusion
In this article, we explored how MapStruct effectively handles nested mappings between complex Java objects and their corresponding DTOs. By explicitly defining mappings with the @Mapping annotation, we ensured that nested properties were accurately mapped across layers.
8. Download the Source Code
This article explored how to perform nested mapping using MapStruct.
You can download the full source code of this example here: mapstruct nested mapping

