Remove PHP array elements without breaking key semantics
An API removes one item from a PHP array, encodes the result, and clients receive a JSON object instead of a JSON list. The deletion worked. The representation changed because PHP arrays are ordered maps: removing an integer key does not automatically close th
An API removes one item from a PHP array, encodes the result, and clients receive a JSON object instead of a JSON list. The deletion worked. The representation changed because PHP arrays are ordered maps: removing an integer key does not automatically close the gap.
First decide whether the identifier is a key or a position
$items = [0 => 'zero', 1 => 'one', 2 => 'two', 3 => 'three'];
unset($items[2]);
var_dump($items);
// keys: 0, 1, 3
The accepted answer by Konrad Rudolph describes several deletion tools. Current PHP documentation confirms the crucial rule: unset() removes the key/value pair and does not reindex the remaining array.
If those keys are identities, preserving them is correct. If the value is logically a JSON list, normalize after deletion:
unset($items[2]);
$items = array_values($items);
echo json_encode($items, JSON_THROW_ON_ERROR);
array_values() discards every existing key and assigns consecutive integers from zero. Never apply it to an associative map whose string or meaningful numeric keys must survive.
Offset deletion is not key deletion
array_splice() removes by positional offset and reindexes numeric keys:
$removed = array_splice($items, 2, 1);
That is appropriate for a list. It is wrong when “2” is a database ID or when sparse keys carry meaning. The function mutates the input and returns the removed values, so assigning its return value back to $items accidentally keeps only what was deleted.
Deleting by value requires an explicit duplicate policy
$key = array_search($needle, $items, true);
if ($key !== false) {
unset($items[$key]);
}
The strict third argument avoids loose comparisons such as numeric strings matching integers. The !== false0array_search() removes only the first match. If every match should go, filter deliberately:
$items = array_values(array_filter(
$items,
static fn ($value) => $value !== $needle
));
array_filter() preserves original keys, so array_values() is still necessary only when the contract requires a list.
Guard the boundary where shape matters
if (!array_is_list($items)) {
throw new LogicException('items must be a zero-based consecutive list');
}
$json = json_encode($items, JSON_THROW_ON_ERROR);
array_is_list() is available in PHP 8.1 and later. On older supported runtimes, compare keys with a consecutive range carefully, including the empty-array case. Validate shape immediately before serialization, cache storage, or a template handoff rather than assuming earlier operations preserved it.
Diagnose the real failure
- Log
array_keys($items), count, and whether the data is intended as list or map; redact values. - Search for
unset,array_filter, and keyed assignments that can introduce gaps. - Check for numeric-string keys: PHP casts valid decimal integer strings in array keys, while other strings remain strings.
- Confirm the exact PHP runtime and client schema.
- Do not confuse
unset($array[$key])withunset($array), which removes the variable binding.
Safe rollout, rollback, and recovery
Add contract tests for deleting the first, middle, and last elements; missing keys; key zero; duplicate values; associative keys; and empty arrays. Assert the encoded JSON shape, not only the PHP values. In staging, compare old and new payload schemas and client parsing.
If a release changes stable identifiers by reindexing, roll back the serializer and restore data from the authoritative store. Do not attempt to infer lost keys from positions unless the mapping was retained. If only a response shape changed, redeploy the previous code and invalidate incompatible cached responses.
unset() is direct and preserves identity. array_values() copies and renumbers the array, costing memory on large datasets. array_splice() expresses positional list editing but also mutates and reindexes. Choose from the data contract first, then measure performance; streaming a large result may be safer than repeatedly copying it.
A hole in numeric keys is not corruption. It becomes a bug only when the next boundary promised a list.
Sources: the Stack Overflow question by Ben and accepted answer by Konrad Rudolph (CC BY-SA), and the PHP manual for array key and removal semantics, unset(), array_splice(), and array_is_list().
Primary source: Review the official reference ↗