package com.example.demo.mapper;
import com.example.demo.dto.UserDTO;
import com.example.demo.model.User;
import org.mapstruct.*;
import java.util.List;
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(source = "email", target = "emailAddress")
@Mapping(target = "fullName", expression = "java(user.getFirstName() + " " + user.getLastName())")
UserDTO toDto(User user);
@Mapping(source = "emailAddress", target = "email")
@Mapping(target = "createdAt", ignore = true)
User toEntity(UserDTO dto);
List<UserDTO> toDtoList(List<User> users);
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
void updateUserFromDto(UserDTO dto, @MappingTarget User user);
}
package com.example.demo.dto;
import lombok.Data;
@Data
public class UserDTO {
private Long id;
private String fullName;
private String emailAddress;
private boolean active;
}
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
private final UserMapper userMapper;
public UserController(UserService userService, UserMapper userMapper) {
this.userService = userService;
this.userMapper = userMapper;
}
@GetMapping
public ResponseEntity<List<UserDTO>> getAllUsers() {
List<User> users = userService.findAll();
List<UserDTO> dtos = userMapper.toDtoList(users);
return ResponseEntity.ok(dtos);
}
@PostMapping
public ResponseEntity<UserDTO> createUser(@RequestBody UserDTO dto) {
User user = userMapper.toEntity(dto);
User saved = userService.save(user);
return ResponseEntity.status(HttpStatus.CREATED)
.body(userMapper.toDto(saved));
}
@PatchMapping("/{id}")
public ResponseEntity<UserDTO> partialUpdate(
@PathVariable Long id,
@RequestBody UserDTO dto
) {
return userService.findById(id)
.map(existing -> {
userMapper.updateUserFromDto(dto, existing);
User updated = userService.save(existing);
return ResponseEntity.ok(userMapper.toDto(updated));
})
.orElse(ResponseEntity.notFound().build());
}
}
MapStruct generates type-safe bean mappers at compile-time, eliminating manual mapping code. I define mapper interfaces with @Mapper annotation—MapStruct generates implementations. @Mapping annotations handle different property names or custom conversions. The approach is faster than reflection-based mappers like ModelMapper because code generation happens at compile-time. Mappers convert between entities and DTOs, preventing exposure of domain models in APIs. Spring integration uses componentModel = "spring" for dependency injection. Collection mappings work automatically. Custom mapping methods handle complex transformations. MapStruct validates mappings at compile-time, catching errors early. The tool reduces boilerplate while maintaining type safety and performance—essential for clean architecture separating layers.