Design DTOs and Mappings Around an API Contract
Databases & persistenceLearn to decouple internal database entities from external API contracts using Java Records and mapping strategies to control data visibility and editability.
On this page
The Boundary Problem
A common mistake in API design is treating the database entity as the communication contract. When a JPA entity is returned directly to a client, the API leaks internal implementation details. More critically, allowing a client to send an entity directly back to the server creates a security vulnerability: if the entity contains a field like loyaltyLevel or accountBalance, a malicious user could include those fields in a JSON request to escalate their privileges, even if the UI doesn’t show those fields.
To solve this, we introduce Data Transfer Objects (DTOs). A DTO is a projection of the data required for a specific use case. It is not a mirror of the database, nor is it necessarily a mirror of the UI. It is a contract. Even if a Request DTO and a Response DTO share the same fields, they should remain distinct because their evolution paths differ: one defines what the server accepts, the other defines what the server promises to provide.
Scenario: Hotel Guest Management
Consider a system where a Guest entity contains sensitive identity data and a server-managed loyalty status. The business rules are:
- Guests can update their contact details (email, phone).
- Guests cannot modify their own
loyaltyLevel. - Public API responses must exclude the
identityDocumentNumberfor privacy.
The Entity Model
@Entity
public class Guest {
@Id @GeneratedValue
private Long id;
private String fullName;
private String email;
private String phone;
private String identityDocumentNumber;
private String loyaltyLevel; // Server-owned
// Getters, setters, etc.
}
The Contract Design
We use Java Records for DTOs because they are immutable, concise, and perfectly suited for data carriers. We define three distinct shapes:
- GuestUpdateRequest: Only contains fields the user is allowed to change.
- GuestResponse: Contains public info, excluding the identity document.
- GuestInternalResponse: (Optional) For admin views, including sensitive data.
// Only editable fields
public record GuestUpdateRequest(
String email,
String phone
) {}
// Publicly visible fields
public record GuestResponse(
Long id,
String fullName,
String email,
String phone,
String loyaltyLevel
) {}
Implementing the Mapping Logic
Mapping is the process of transforming an entity into a DTO (and vice versa). While libraries exist, explicit mapping provides the most control over business rules.
Worked Example: The Mapping Service
@Service
public class GuestMapper {
public GuestResponse toResponse(Guest guest) {
return new GuestResponse(
guest.getId(),
guest.getFullName(),
guest.getEmail(),
guest.getPhone(),
guest.getLoyaltyLevel()
);
}
public void updateEntityFromDto(GuestUpdateRequest dto, Guest guest) {
// We explicitly ignore loyaltyLevel here
if (dto.email() != null) guest.setEmail(dto.email());
if (dto.phone() != null) guest.setPhone(dto.phone());
}
}
Trace of a Request
- Request: Client sends
PUT /guests/1with body{"email": "new@email.com", "loyaltyLevel": "PLATINUM"}. - Binding: Spring binds the JSON to
GuestUpdateRequest. Because the record does not have aloyaltyLevelcomponent, the extra JSON field is ignored by the message converter. - Processing: The service fetches the
Guestentity viafindById. TheGuestMapperupdates only the email and phone. - Persistence: The updated entity is saved.
- Response: The service maps the updated entity to
GuestResponse. TheidentityDocumentNumberis never included in the record constructor, ensuring it never leaves the server.
Failure Cases and Consequences
- The “Pass-Through” Failure: If you use the same DTO for both request and response, you might accidentally expose the
idas editable or require the client to send theloyaltyLevelback just to update a phone number. - The “Entity Leak” Failure: Returning the
Guestentity directly. If a new fieldinternalNotesis added to the database for staff use, it is automatically leaked to the API response unless explicitly marked with@JsonIgnore. Using a DTO makes this leak impossible by design. - The “Null Overwrite” Failure: In the
updateEntityFromDtomethod, if you simply callguest.setEmail(dto.email())without a null check, a client omitting the email field in a partial update would overwrite a valid email withnullin the database.
Exercise
Scenario: You are adding a GuestRegistrationRequest DTO. The registration requires fullName, email, and identityDocumentNumber. However, the GuestResponse must still exclude the identityDocumentNumber.
Task: Define the GuestRegistrationRequest record and explain why it cannot be reused as the GuestResponse.
Answer:
public record GuestRegistrationRequest(
String fullName,
String email,
String identityDocumentNumber
) {}
It cannot be reused as the GuestResponse because the registration request requires the identityDocumentNumber for creation, but the response must exclude it for security/privacy. Reusing the record would either force the API to leak the document number or prevent the user from registering.