The Update Problem REST Doesn't Solve
Does your backend know the difference between {} and {"email": null}? It's vital if you want the correct data. Missing fields and explicit nulls carry different intent.
Join the DZone community and get the full member experience.
Join For FreeConsider the following two requests:
{}
and
{ "email": null }
They look similar, but they are fundamentally different. In the first case, nothing is said about email; in the second case, email is explicitly set to null.
So, what should the system do? And how is that behavior defined today? Most systems cannot answer this precisely. They rely on conventions, mapping frameworks, or implicit behavior.
This is not a limitation of JSON or JPA. It is the absence of a defined write model.
The Core Problem
A system that cannot distinguish between a field what was:
- Not sent
- Sent with null
- Sent with a value
does not have a well-defined way to update data. Without this distinction, the system must interpret the request.
And interpretation introduces ambiguity. A missing field may:
- Be ignored
- Overwrite existing data
- Be treated differently depending on the mapping layer
None of this is visible in the API. The behavior exists, but it is not defined.
A Real Failure Scenario
Assume the current state is:
- name = "Anna"
- email = "[email protected]"
A client sends: {"name":"Anna"}
A common implementation looks like this:
public void updateUser(UserDTO dto, User entity) {
entity.setName(dto.getName());
entity.setEmail(dto.getEmail());
}
What happens to email?
- If the mapper sets null → email is overwritten
- If the mapper ignores null → email is preserved
- If behavior changes later → the result changes
The API does not define this. The framework does.
A refactoring, a new mapper, or a configuration change may alter the outcome without changing the request. This is not a client error; it is undefined behavior.
Common Approaches
Several approaches try to address partial updates:
- Full updates (PUT): Require complete objects
- Partial updates (PATCH): Allow partial structures
- JSON merge patch: Merges values into existing state
- Mapping frameworks: Copy values into entities
All of these operate on structure; none of them define intent. They describe what data looks like, not what the client meant.
A Simple Principle
To achieve correctness, the system must follow a simple rule: Only explicit intent may change state. If the client does not express intent, the system must not infer it.
This leads to three cases:
- Field not present → no intent → no change
- Field present with value → update
- Field present with null → clear
The distinction is not in the value itself, but in whether the field was present.
Making Intent Explicit
To implement this, the system must track presence per field.
A minimal example:
public class UserDTO {
private String email;
private boolean emailPresent;
public void setEmail(String email) {
this.email = email;
this.emailPresent = true;
}
public boolean isEmailPresent() {
return emailPresent;
}
public String getEmail() {
return email;
}
}
Now the backend can apply changes explicitly:
public void updateUser(UserDTO dto, User entity) {
if (dto.isEmailPresent()) {
entity.setEmail(dto.getEmail());
}
}
The behavior is now defined:
- Not present → no change
- Present null → clear
- Present value → update
No interpretation is required.
Where It Breaks First
Booleans: {"active":false}
Is false an explicit value, or just a default? Without presence tracking, the system cannot tell.
Collections: {"roles":[]}
Does this mean:
- Clear all roles
- Or ignore roles
With explicit presence:
if (dto.isRolesPresent()) {
entity.getRoles().clear();
entity.getRoles().addAll(dto.getRoles());
}
The meaning is clear:
- omitted → no change
- [] → clear
- [x,y] → replace
These are not edge cases; they are normal updates.
Structural Consequence
The backend does not detect client mistakes. It cannot know whether a field was omitted intentionally or by accident. What reaches the backend is the only expressed intent.
The system can:
- Validate values
- Enforce invariants
- Reject invalid states
But it must not guess missing intent. A system that treats missing data as instructions is making decisions on behalf of the client.
This is where incorrect writes begin.
What This Solves
With explicit intent:
- Missing fields never overwrite existing data
- Null values are consistently interpreted as clear
- Collections behave predictably
- Updates are stable across refactoring and framework changes
Most importantly:
The system no longer interprets requests; it executes them.
What This Does NOT Solve
This approach does not address:
- Business validation
- Concurrency control
- Client-side errors
- Default values
Those concerns remain in the domain and persistence layers. This pattern addresses only one problem: ambiguity in write intent.
Trade-Offs
This approach introduces additional structure. It requires:
- Tracking presence per field or segment
- Explicit update logic in the backend
- More code than direct mapping
In other words, it adds boilerplate. But the trade-off is clear:
- No implicit behavior
- No ambiguity
- No unintended data loss
Conclusion
There are multiple ways to interpret partial updates.
A system can:
- Ignore missing fields
- Reject incomplete requests
- Apply defaults
- Attempt to merge state
Each choice has consequences. This approach makes one decision explicit: The system does not guess what the client meant; it only acts on what is explicitly sent. If a field is missing, no intent was expressed, and without intent, nothing should change.
The question is not how your API handles updates; the question is whether that behavior is defined or left to chance.
Opinions expressed by DZone contributors are their own.
Comments