Spring Boot DTO and Mapping
A DTO, short for Data Transfer Object, is a simple class that carries data between layers or across the network. DTOs describe exactly what an API accepts and what it returns. Entities describe database tables. Keeping the two apart protects private fields, keeps the API stable, and prevents many hidden bugs.
Mapping is the step that converts an entity into a DTO and back again.
The Restaurant Bill Idea
A restaurant kitchen holds a full record for every dish: ingredients, supplier names, cost price, and profit margin. A guest receives only a bill that lists dish names and prices. The bill is a DTO. It shows what the guest needs and hides internal details. The kitchen record is the entity.
Kitchen record (Entity) Guest bill (DTO) +---------------------------+ +--------------------+ | id | | name | | name | ---> | price | | price | +--------------------+ | supplierName | | costPrice | Only safe fields leave the kitchen | profitMargin | +---------------------------+
The Problem with Exposing Entities
Returning an entity straight from a controller causes trouble.
- Sensitive fields such as passwords or internal notes leak into the JSON reply.
- Any change to the table changes the public API and can break client apps.
- Lazy relationships trigger extra queries or endless JSON loops.
- Clients can send fields they should never control, such as id or role. This risk has the name mass assignment.
A User Example
The entity holds the password hash and the role.
@Entity
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private String passwordHash;
private String role;
// getters and setters
}Request and Response DTOs
Separate classes handle input and output. Records keep them short.
public record UserCreateRequest(
@NotBlank String name,
@Email String email,
@Size(min = 8) String password) {
}
public record UserResponse(Long id, String name, String email) {
}The request DTO accepts only three fields and carries validation rules. The response DTO returns three safe fields and leaves out the password hash and role.
Client --> UserCreateRequest --> Controller --> Service --> AppUser (entity) Client <-- UserResponse <-- Controller <-- Service <-- AppUser (entity)
Manual Mapping
A small mapper class converts the objects by hand. The approach needs no library and reads clearly.
@Component
public class UserMapper {
public AppUser toEntity(UserCreateRequest request, String passwordHash) {
AppUser user = new AppUser();
user.setName(request.name());
user.setEmail(request.email());
user.setPasswordHash(passwordHash);
user.setRole("USER");
return user;
}
public UserResponse toResponse(AppUser user) {
return new UserResponse(user.getId(), user.getName(), user.getEmail());
}
}The mapper sets the role on the server. Clients never choose it.
Using the DTOs in a Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService service;
public UserController(UserService service) {
this.service = service;
}
@PostMapping
public ResponseEntity<UserResponse> create(@Valid @RequestBody UserCreateRequest request) {
UserResponse created = service.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@GetMapping("/{id}")
public UserResponse find(@PathVariable Long id) {
return service.findById(id);
}
}Automatic Mapping with MapStruct
Large projects have dozens of DTOs, and manual mappers grow tedious. MapStruct generates mapper code at compile time. The generated code runs as fast as hand-written code and shows compile errors for mismatched fields.
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>Add the mapstruct-processor artifact to the annotation processor paths of the Maven compiler plugin. Then define an interface.
@Mapper(componentModel = "spring")
public interface UserMapper {
UserResponse toResponse(AppUser user);
@Mapping(target = "id", ignore = true)
@Mapping(target = "passwordHash", ignore = true)
@Mapping(target = "role", constant = "USER")
AppUser toEntity(UserCreateRequest request);
List<UserResponse> toResponseList(List<AppUser> users);
}MapStruct matches fields by name and builds the implementation class during compilation. Inject the interface like any Spring bean.
Choosing a Mapping Style
| Style | Strength | Weakness |
|---|---|---|
| Manual mapper | Clear, no library | Repetitive in large projects |
| MapStruct | Fast, checked at compile time | Needs processor setup |
| ModelMapper | Quick to start | Matches at runtime, so errors appear late |
Updating with Partial Data
Update requests often change only a few fields. Use a dedicated update DTO, and copy only the allowed values onto the loaded entity.
public record UserUpdateRequest(String name, String email) {
}
public UserResponse update(Long id, UserUpdateRequest request) {
AppUser user = repository.findById(id).orElseThrow(() -> new UserNotFoundException(id));
if (request.name() != null) user.setName(request.name());
if (request.email() != null) user.setEmail(request.email());
return mapper.toResponse(repository.save(user));
}Why Records Fit DTOs
Records give DTOs a compact shape. Each record declares its fields once and receives a constructor, accessors, equals, hashCode, and toString automatically. Immutability protects the data after creation, so no code changes a response by accident. Jackson reads and writes records without extra setup in current Spring Boot versions.
Best Practices
- Create separate request and response DTOs for each use case.
- Place validation annotations on request DTOs.
- Map inside the service layer, and keep controllers free of entity classes.
- Choose names such as UserResponse and UserCreateRequest to show intent.
- Never copy fields such as role or id straight from client input.
DTOs form a clean border around the database model. Applications that use them change faster, leak less, and stay easier to maintain.
