Core Java

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 Library contains a list of Song objects.
  • Each Song includes a Track object.
  • The Track includes details like artistName, albumName, and duration.

We want to map this into a flat DTO structure where:

  • SongDTO includes title, album, artist, and a nested TrackDTO for duration.

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.artistName was mapped to SongDTO.artist and then back again.
  • Track.albumName was mapped to SongDTO.album and then back again.
  • Track.duration was preserved through nested mapping via TrackDTO.

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.

Download
You can download the full source code of this example here: mapstruct nested mapping

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button