iTranslated by AI
Avoid Mixing Null, Omitted Keys, and Empty Strings in JSON Responses
Introduction
When representing "no value" in an API JSON response, there are three common approaches: null, omitting the key, or using an empty string. Since all of these work, it's easy to be ambiguous, but mixing them will inevitably break the implementation on the receiving side.
To give you the conclusion first: you should distinguish them by meaning—null for unset, omitting the key for not applicable, and "" for empty values—and prioritizing consistency within your API is paramount.
Why are they mixed?
The reason is that the representation of "no value" differs across layers:
- Databases return
NULL. - Go API servers omit keys using
omitempty. - Front-end expects an empty string.
- Response structures differ between GET and PATCH.
If each layer implements this based on its own judgment, the same field might arrive as null or be omitted depending on the call site.
Differences in meaning between the three
// null: The value does not exist (unset or not registered)
{ "name": null }
// Key omitted: The field itself is not applicable to this response
{}
// Empty string: Registered as empty
{ "name": "" }
These three have different meanings. Mixing them forces the receiving side to account for all possible cases.
Accidents caused by mixing
- If
nulland key omission are mixed, the front-end needs both existence checks (via?.) andnullchecks. - If empty strings and
nullare mixed, the distinction between "not entered" and "registered as empty" disappears. - It becomes ambiguous whether a field omitted in a PATCH request should be interpreted as "no change" or "set to null."
PATCH requests are particularly prone to this.
// Want to update name to null
{ "name": null }
// Do not change name
{}
If an API that returns null in a GET request uses omitempty in a PATCH request, sending null may be treated as an omission and ignored as "no change." When the behavior of the same field changes between GET and PATCH, it becomes difficult for the front-end to track what is happening.
Basic design principles
Consistency is the top priority. Based on that, here is a guideline for decision-making:
| Situation | Recommendation |
|---|---|
| Value does not exist (unset) | null |
| Field not applicable to this response | Omit the key |
| Registered as empty | "" |
Frequent use of key omission makes types unstable. If you define types in TypeScript, every field becomes T | undefined, requiring existence checks every time you access them. If your front-end type definitions become an "optional hell," it is almost always caused by API field omission. If you simply want to represent an unset state, null is easier to handle and makes types more stable.
The same problem exists with arrays and objects
This applies not only to scalar values but also to arrays and objects.
// Representing an array without elements
{ "items": null } // Not fetched / Not applicable
{ "items": [] } // Fetched, but 0 items
null and an empty array have different meanings. It is natural to return [] if the fetch was performed but yielded no results, and null (or omission) if the data is not intended to be fetched in the first place. If these are mixed, the receiving side cannot treat them uniformly.
Concrete example: Application date for monthly subscriptions
This is a classic field where the state is represented by the presence or absence of a value. Consider an API that returns the monthly application status in applied_month. If an application exists, return the application month in yyyy-MM-01 format; if no application exists, return null.
// Applied
{ "applied_month": "2025-03-01" }
// Not applied
{ "applied_month": null }
applied_month is not the "date itself" but "a value representing the month in which an application exists." Since the presence or absence of the value represents the application state, null matches both the meaning and the type.
Empty strings are NG because they contradict the yyyy-MM-01 format. Key omission should be avoided because it becomes impossible to distinguish whether the field "does not exist in the API specification" or the user is "not applied."
If the status increases in the future (e.g., expired, cancelled), one field will no longer be enough to represent the status. In that case, a design that separates the status from the value is safer.
{
"apply_status": "NOT_APPLIED",
"applied_month": null
}
Implementation in Go
To return null, use a pointer.
type UserResponse struct {
Name *string `json:"name"`
}
If the value exists, put &name; if it does not, put nil, and it will be serialized as null.
To omit the key, use omitempty.
type UserResponse struct {
Name *string `json:"name,omitempty"`
}
omitempty omits the key when the pointer is nil. Be aware that if you need both null and omission, adding omitempty will prevent you from returning null.
Fixing the specification with OpenAPI
Trying to unify behavior through implementation alone has its limits. By explicitly defining nullable and required in your OpenAPI schema, you can prevent discrepancies between the specification and the implementation.
name:
type: string
nullable: true
Including a field in required means it cannot be omitted, while nullable: true means null is allowed. Explicitly stating this in the specification also reduces misunderstandings within the team.
Summary
-
null, key omission, and empty strings have different meanings. - Mixing them is the worst approach; consistency is paramount.
- Making
nullthe first choice for "unset" states makes it easier for the receiving side to handle. - Align the meanings of
nulland[]for arrays as well. - Fixing specifications with an OpenAPI schema prevents fluctuations.
Discussion