iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🤖

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 null and key omission are mixed, the front-end needs both existence checks (via ?.) and null checks.
  • If empty strings and null are 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 null the first choice for "unset" states makes it easier for the receiving side to handle.
  • Align the meanings of null and [] for arrays as well.
  • Fixing specifications with an OpenAPI schema prevents fluctuations.

Discussion