Lập trình · 26/09/2026

PHP array_filter and JSON: Why Lists Become Objects

A frontend calls map on API results and fails because the response is no longer an array. The backend still uses a PHP array and only added a filter. The remaining numeric keys may explain the change.

PHP array_filter và JSON: Vì sao danh sách biến thành object?

A frontend calls map on API results and fails because the response is no longer an array. The backend still uses a PHP array and only added a filter. The remaining numeric keys may explain the change.

1. Reproduce it with three entries

$labels = ['Draft', 'Hidden', 'Published'];
$visible = array_filter(
    $labels,
    static fn (string $label): bool => $label !== 'Hidden'
);

echo json_encode($visible, JSON_THROW_ON_ERROR);
// {"0":"Draft","2":"Published"}

The array_filter documentation states that retained entries keep their keys. The result therefore has keys 0 and 2, not 0 and 1.

The json_encode reference distinguishes consecutive numeric keys starting at zero from nonconsecutive keys. This example becomes a JSON object even though the PHP variable remains an array.

2. Reindex when the contract requires a list

$payload = ['items' => array_values($visible)];
echo json_encode($payload, JSON_THROW_ON_ERROR);
// {"items":["Draft","Published"]}

array_values returns values with fresh numeric indices. Here, items is deliberately a list, so discarding old keys is appropriate. With no remaining entries, items becomes [].

Avoid making clients accept both arrays and objects when the API promises a list. That can hide a server defect and force every consumer to guess the format.

3. Do not reindex lookup maps blindly

Keys in a user-ID lookup carry business meaning. Removing them loses the association between each identifier and its value:

$namesById = [41 => 'Lan', 73 => 'Minh'];
// array_values($namesById) would discard IDs 41 and 73.

Ask whether the recipient needs an ordered list or a keyed mapping. When both identity and list behavior matter, consider records with explicit id fields rather than encoding identity in array positions.

4. Make filtering intent explicit

Without a callback, array_filter removes values considered empty by PHP, including numeric zero and the string '0'. If the task is only to remove null, state that condition:

$values = [0, '0', null, 'Ready'];
$items = array_values(array_filter(
    $values,
    static fn ($value): bool => $value !== null
));

Filtering and list formatting are separate steps. Test them independently so a structurally correct response does not silently lose meaningful values.

5. Test shape, not just element counts

  • Keeping every entry preserves a JSON array.
  • Removing the first or middle entry does not create an object.
  • Removing everything yields [], not null or {}.
  • One remaining entry is still a list, not a scalar.
  • Zero and '0' survive when allowed by the contract.
  • Lookup maps retain identifiers under their own contract.

Integration tests should inspect the serialized response clients actually receive. Counting PHP array entries alone cannot detect a change from [] to {}. These examples require PHP 7.4 or later; JSON_THROW_ON_ERROR reports encoding failures through exceptions but does not validate your data contract.

A small normalization step at the serialization boundary makes an API more predictable when the distinction between lists and lookup maps is explicit.

Discussion

Comments 0

Sign in to comment

You need an account to join the discussion and reply to other readers.

Sign inRegister

No comments yet. Be the first to share your thoughts.