Empty Array vs Null vs Missing Field in JSON APIs
Leaving out empty arrays saves a few bytes and costs every client a defensive check. Here is how to decide what an API should return, and how to write it into the contract.
Should a JSON API return an empty array, null, or leave the field out? The Remove Empty Values from JSON tool prunes empty strings, arrays and objects from a document, and its page describes where that helps, such as cleaning form submissions before storage. An API response is a different case, because someone else's code reads it.
This guide is about that decision: whether an API should return "items": [] or leave items out, and the same question for strings and nulls.
What a client does with a missing field
Most client code is written against the response someone saw while building it. If that response had "items": [], the code reads items directly. Leave the field out and it fails, often far from the API call:
JavaScript response.items.length TypeError: Cannot read properties of undefined (reading 'length')
JavaScript for (const i of response.items) TypeError: response.items is not iterable
Python len(response["items"]) KeyError: 'items'The fix on the client side is easy, response.items ?? [] or response.get("items", []), but every client has to know to write it, for every field that might be omitted. Generated clients from OpenAPI or JSON Schema mark such fields optional, which pushes the same check into every call site.
Empty array vs null vs missing field
The three forms are not interchangeable. Each tells the client something different:
"items": []: the order has no items. The value is known, and it is empty."coupon": null: there is no coupon, or it is unknown. The field exists in the model, and it has no value.couponleft out: the server did not include it. That can mean not applicable, not requested, or not supported by this API version, and the client cannot tell which.
Strings follow the same pattern. A middle name of "" says the customer has none. null says nobody asked. An API that removes both collapses two answers into one, and the information cannot be recovered on the client.
What common formats do
- Protocol Buffers (proto3) mapped to JSON leaves out fields that hold their default value by default: empty lists, empty strings, zero and false. JSON APIs generated from protobuf definitions behave this way unless the serializer is configured to emit defaults, and their clients treat a missing field as its default.
- GraphQL returns every field the query asked for, with
nullor[]as the schema allows. Clients never meet a missing field they requested. - Hand-written REST APIs vary. Many return empty arrays and use
nullfor missing scalar values, so the response shape stays the same for every resource.
The protobuf approach works because both sides share a schema that defines every default. Without a shared schema, clients have to guess, and that is where omission causes bugs.
Effect on diffs, tests and size
When fields come and go depending on their value, responses for similar resources have different shapes. Snapshot tests, contract tests and diffs between environments then show fields appearing and disappearing that mean nothing. A stable shape keeps those tools quiet until something real changes.
The size argument for omission is weak. "tags":[], is ten bytes, and with gzip or Brotli on the response, repeated keys cost even less. It rarely outweighs the cost of a client crash.
A rule worth adopting
- Always return arrays, even when they are empty.
- Return
nullfor a scalar value that is unknown or not set, rather than leaving the field out. - Omit a field only when it does not apply to the resource at all, or when the client explicitly asked for a subset of fields.
- Write the rule into the schema, so generated clients and validators enforce it.
In JSON Schema, list the fields as required and allow null where it is a valid answer. A validator then rejects a response that drops items or sends it as null:
{
"type": "object",
"required": ["id", "items", "coupon"],
"properties": {
"id": { "type": "string" },
"items": { "type": "array", "items": { "type": "object" } },
"coupon": { "type": ["string", "null"] }
}
}{"id":"ord_1","items":[],"coupon":null} valid
{"id":"ord_1","coupon":"SAVE10"} / must have required property 'items'
{"id":"ord_1","items":null,"coupon":null} /items must be arrayRun that validation in the API's own tests, so a serializer setting that starts omitting empty arrays fails a build instead of a client. The JSON Schema Validator checks a single response against a schema in the browser.
Related tools
Recursively strip every null value from a JSON document.
Pretty-print JSON with configurable indentation and instant validation.
Strip whitespace to shrink JSON to the smallest valid payload.
Deep-merge two or more JSON objects into one.
Related guides
A JSON.stringify replacer removes most nulls but not all of them. Here is a tested recursive function for JavaScript and Python, and the one case where removing nulls breaks things.
Two formatters can disagree about the same JSON file. Here is how a team picks one, applies it in every editor and before every commit, and makes CI enforce it.
Whitespace in JSON is nearly free once gzip runs. Here is where JSON travels without compression, with the extra bytes measured on sample payloads.
The built-in ways to combine objects lose nested settings. Here is a short, tested deep merge for JavaScript and Python, and how to handle arrays, null and __proto__.