Search code examples
javaspring-bootspring-data-jpadtomapstruct

MapStruct mapper not mapping nested DTOs properly


ISSUE : Mapstruct mapper for User DTO not returning null value for a deeply nested DTO field in JSON (UserDto class has a PlaylistDto property, whereas PlaylistDto class itself consists a SongDto field).

Project OVREVIEW : I am having some trouble while mapping nested DTO field properly with mapstruct. I m working on a music streaming springBoot application which deals with several entities - User , Playlist and Song. There is one-to-many association between User and Playlist (User is the owning side) and a many-to-many association between Playlist (the owning side) and Song entities. The Entities are as follows :

User Entity

@Entity
@Table(name = "users")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,property = "id")

public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id ;

    private String username ;
    private String password ;
    private String email ;
    private String firstname ;
    private String lastname ;

    @OneToMany(
            mappedBy = "user" ,
            cascade = {CascadeType.PERSIST , CascadeType.MERGE} ,
            orphanRemoval = true
    )
    private List<Playlist> playlists = new ArrayList<>() ;

    // getters, setters and constructors ...
} 

Playlist Entity

@Entity
@Table(name = "playlist")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,property = "id")
public class Playlist {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id ;

    private String name ;
    private String description ;
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "user_id")
    private User user ;

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(
            name = "playlist_song" ,
            joinColumns = {@JoinColumn(name = "playlist_id")} ,
            inverseJoinColumns = {@JoinColumn(name = "song_id")}
    )
    private Set<Song> songs = new HashSet<>() ;

    // getters, setters and constructors ...
}

Song Entity

@Entity
@Table(name = "song")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,property = "id")
public class Song {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id ;

    private String name ;
    private String artist ;
    private String album ;
    private Integer year ;
    private String genre ;
    private Integer duration ;

    @ManyToMany(mappedBy = "songs")
    private Set<Playlist> playlists = new HashSet<>() ;

    // getters, setters and constructors ...

The DTOs for the above entities are as follows :

User DTO

public class UserDto {
    private Long id ;
    private String username ;
    private String email ;
    private String firstname ;
    private String lastname ;

    private List<PlaylistDto> playlistsDto ;

    // constructors
    public UserDto() {
    }

    public UserDto(Long id, String username, String email, String firstname, String lastname, List<PlaylistDto> playlistsDto) {
        this.id = id;
        this.username = username;
        this.email = email;
        this.firstname = firstname;
        this.lastname = lastname;
        this.playlistsDto = playlistsDto;
    }

    // getters and setters ...
}

Playlist DTO

public class PlaylistDto {
    private Long id ;
    private String name ;
    private String description ;
    private Set<SongDto> songsDto;

    // constructors

    public PlaylistDto() {
    }

    public PlaylistDto(Long id, String name, String description, Set<SongDto> songsDto) {
        this.id = id;
        this.name = name;
        this.description = description;
        this.songsDto = songsDto;
    }

    // getters & setters
}

Song DTO

public class SongDto {
    private Long id ;
    private String name ;
    private String artist ;
    private String album ;

    // constructors

    public SongDto() {
    }

    public SongDto(Long id, String name, String artist, String album) {
        this.id = id;
        this.name = name;
        this.artist = artist;
        this.album = album;
    }

    // getters & setters
}

Lastly, here are the mappers :

// User Mapper
@Mapper(componentModel = "spring" , uses = {PlaylistDto.class , SongDto.class})
@Component
public interface UserMapper {

    UserMapper MAPPER = Mappers.getMapper(UserMapper.class);

    @Mapping(source = "playlists", target = "playlistsDto")
    UserDto usertoUserDto(User user) ;

    @Mapping(source = "playlistsDto", target = "playlists")
    User userDtoToCustomer(UserDto userDto) ;
}


// Playlist Mapper
@Mapper(componentModel = "spring" , uses = {SongDto.class})
@Component
public interface PlaylistMapper {

    PlaylistMapper MAPPER = Mappers.getMapper(PlaylistMapper.class) ;
    @Mapping(source = "songs" , target = "songsDto")
    PlaylistDto playlistToDto (Playlist entity) ;

    @Mapping(source = "songsDto" , target = "songs")
    Playlist toEntity (PlaylistDto dto) ;
}


// Song Mapper 
@Mapper(componentModel = "spring")
@Component
public interface SongMapper {
    SongMapper MAPPER = Mappers.getMapper(SongMapper.class) ;

    SongDto songToDto (Song entity) ;

    Song toEntity (SongDto dto) ;
}

The mapstruct generated Mapper implementations:

Playlist MapperImpl

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-04-04T11:16:24+0530",
    comments = "version: 1.5.3.Final, compiler: javac, environment: Java 19.0.2 (Oracle Corporation)"
)
@Component
public class PlaylistMapperImpl implements PlaylistMapper {

    @Override
    public PlaylistDto playlistToDto(Playlist entity) {
        if ( entity == null ) {
            return null;
        }

        PlaylistDto playlistDto = new PlaylistDto();

        playlistDto.setSongsDto( songSetToSongDtoSet( entity.getSongs() ) );
        playlistDto.setId( entity.getId() );
        playlistDto.setName( entity.getName() );
        playlistDto.setDescription( entity.getDescription() );

        return playlistDto;
    }

    @Override
    public Playlist toEntity(PlaylistDto dto) {
        if ( dto == null ) {
            return null;
        }

        Playlist playlist = new Playlist();

        playlist.setSongs( songDtoSetToSongSet( dto.getSongsDto() ) );
        playlist.setName( dto.getName() );
        playlist.setDescription( dto.getDescription() );

        return playlist;
    }

    protected SongDto songToSongDto(Song song) {
        if ( song == null ) {
            return null;
        }

        SongDto songDto = new SongDto();

        songDto.setId( song.getId() );
        songDto.setName( song.getName() );
        songDto.setArtist( song.getArtist() );
        songDto.setAlbum( song.getAlbum() );

        return songDto;
    }

    protected Set<SongDto> songSetToSongDtoSet(Set<Song> set) {
        if ( set == null ) {
            return null;
        }

        Set<SongDto> set1 = new LinkedHashSet<SongDto>( Math.max( (int) ( set.size() / .75f ) + 1, 16 ) );
        for ( Song song : set ) {
            set1.add( songToSongDto( song ) );
        }

        return set1;
    }

    protected Song songDtoToSong(SongDto songDto) {
        if ( songDto == null ) {
            return null;
        }

        Song song = new Song();

        song.setName( songDto.getName() );
        song.setArtist( songDto.getArtist() );
        song.setAlbum( songDto.getAlbum() );

        return song;
    }

    protected Set<Song> songDtoSetToSongSet(Set<SongDto> set) {
        if ( set == null ) {
            return null;
        }

        Set<Song> set1 = new LinkedHashSet<Song>( Math.max( (int) ( set.size() / .75f ) + 1, 16 ) );
        for ( SongDto songDto : set ) {
            set1.add( songDtoToSong( songDto ) );
        }

        return set1;
    }
}

User MapperImpl

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-04-04T11:16:25+0530",
    comments = "version: 1.5.3.Final, compiler: javac, environment: Java 19.0.2 (Oracle Corporation)"
)
@Component
public class UserMapperImpl implements UserMapper {

    @Override
    public UserDto usertoUserDto(User user) {
        if ( user == null ) {
            return null;
        }

        UserDto userDto = new UserDto();

        userDto.setPlaylistsDto( playlistListToPlaylistDtoList( user.getPlaylists() ) );
        userDto.setId( user.getId() );
        userDto.setUsername( user.getUsername() );
        userDto.setEmail( user.getEmail() );
        userDto.setFirstname( user.getFirstname() );
        userDto.setLastname( user.getLastname() );

        return userDto;
    }

    @Override
    public User userDtoToCustomer(UserDto userDto) {
        if ( userDto == null ) {
            return null;
        }

        User user = new User();

        user.setPlaylists( playlistDtoListToPlaylistList( userDto.getPlaylistsDto() ) );
        user.setUsername( userDto.getUsername() );
        user.setEmail( userDto.getEmail() );
        user.setFirstname( userDto.getFirstname() );
        user.setLastname( userDto.getLastname() );

        return user;
    }

    protected PlaylistDto playlistToPlaylistDto(Playlist playlist) {
        if ( playlist == null ) {
            return null;
        }

        PlaylistDto playlistDto = new PlaylistDto();

        playlistDto.setId( playlist.getId() );
        playlistDto.setName( playlist.getName() );
        playlistDto.setDescription( playlist.getDescription() );

        return playlistDto;
    }

    protected List<PlaylistDto> playlistListToPlaylistDtoList(List<Playlist> list) {
        if ( list == null ) {
            return null;
        }

        List<PlaylistDto> list1 = new ArrayList<PlaylistDto>( list.size() );
        for ( Playlist playlist : list ) {
            list1.add( playlistToPlaylistDto( playlist ) );
        }

        return list1;
    }

    protected Playlist playlistDtoToPlaylist(PlaylistDto playlistDto) {
        if ( playlistDto == null ) {
            return null;
        }

        Playlist playlist = new Playlist();

        playlist.setName( playlistDto.getName() );
        playlist.setDescription( playlistDto.getDescription() );

        return playlist;
    }

    protected List<Playlist> playlistDtoListToPlaylistList(List<PlaylistDto> list) {
        if ( list == null ) {
            return null;
        }

        List<Playlist> list1 = new ArrayList<Playlist>( list.size() );
        for ( PlaylistDto playlistDto : list ) {
            list1.add( playlistDtoToPlaylist( playlistDto ) );
        }

        return list1;
    }
}

The unexpected behavior

I have also setup controllers for retrieving the JSON objects for Playlists in database as well as Users in database. The endpoint for `GET playlists (DTOs)` gives the expected JSON object, since the **nested songDto field** doesn't return null for every playlist object, as you can see in the image below :

On the contrary, for the endpoint with GET request to fetch all users (DTOs), the nested playlistDto field seems to be working fine but the inner nested songsDto field return null for every userDto JSON object. This behavior shouldn't happen (as there are songs associated with some playlists which can be seen in the output of GET playlists endpoint's output). Below is an image for the JSON object for GET users request :


Why is this null value problem happening only while fetching all UsersDto but not when fetching all playlistsDto and how to fix this.


Solution

  • You are using the uses field in your mapper config wrong. You are refering to a DTO.

    @Mapper(componentModel = "spring" , uses = {PlaylistDto.class , SongDto.class})
    @Component
    public interface UserMapper {
    

    According to the documentation you need to refer to another mapper that should be used by the configured mapper

    /**
     * Other mapper types used by this mapper. May be hand-written classes or other mappers generated by MapStruct. No
     * cycle between generated mapper classes must be created.
     *
     * @return The mapper types used by this mapper.
     */
    Class<?>[] uses() default { };
    

    Your solution should look like this:

    @Mapper(componentModel = "spring" , uses = {PlaylistMapper.class , SongMapper .class})
    @Component
    public interface UserMapper {
    

    As a sidenote you dont have to call Mappers.get( in your shown classes. This reference is never used. It is referened with the uses config.