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 :
@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 ...
}
@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 ...
}
@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 ...
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 ...
}
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
}
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) ;
}
@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;
}
}
@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;
}
}
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.
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.