Emit "data": [] for a list that found nothing

Merged
RO
15h ago
+61-1

What changes for callers

An empty basecamp todos list printed an envelope with no data field, while an empty messages list printed "data": []. In a live agent eval, the agent's --jq '[.data[] | ...]' exited 1 with cannot iterate over: null, and it told the user the listing had failed. After this change, every command that hands the output layer a nil slice emits "data": [].

Before and after, run against a local bc3 (todos list -p 311619837 --status archived; the API returns [] for that list):

origin/mainthis branch
--json{"ok":true,"summary":"0 todos",...} (no data){"ok":true,"data":[],"summary":"0 todos",...}
--jq '[.data[] | .id]'exit 1, cannot iterate over: null[], exit 0
--count10

Cause

Writer.OK normalizes data with a JSON round-trip. A nil []basecamp.Todo marshals to null, unmarshals to nil, and omitempty on Response.Data drops the field. A nil []map[string]any skips the round-trip and printed "data": null instead. todos list across all lists builds its result with var result []basecamp.Todo and appends, so it stays nil when nothing matches. Other commands build lists the same way, so the fix belongs in the envelope, not in each command.

Fix

Writer.OK turns a nil slice into an empty slice of the same type before anything else reads it. This covers the JSON, --jq, --agent/--quiet, --count, styled and Markdown outputs. Styled and Markdown now show the empty-list rendering (No results) in place of (no data) / No data, the same as commands that already returned empty slices.

What stays the same:

  • Untyped nil and nil pointers stay absent.
  • Nil maps stay null.
  • Byte slices are excluded. A nil json.RawMessage means "no body" and still prints null, because an empty RawMessage is invalid JSON and would fail to encode.

Tests

  • TestWriterOKNilSliceIsEmptyArray covers a nil []basecamp.Todo in JSON and quiet mode, and a nil []map[string]any. It fails on main.
  • TestWriterOKNilNonSliceUnchanged pins the existing output for untyped nil, a nil pointer, a nil map and a nil json.RawMessage.
  • TestTodosListEmptyEmitsEmptyDataArray runs todos list across lists and for a single list, with no matching to-dos. The across-lists case fails on main. The single-list case already passed and guards against a regression.

bin/ci is green locally.


Summary by cubic

Fixes empty list results so they emit "data": [] instead of omitting the field or printing null. A nil slice was normalized to null and then dropped by omitempty, so todos list with no matches had no data field at all — --jq '[.data[] | ...]' failed with "cannot iterate over: null" and --count printed 1 instead of 0.

Writer.OK now converts a nil slice to an empty slice of the same type before writing the response, so every command that returns an empty list behaves consistently across JSON, quiet, count, styled, and markdown outputs. Untyped nil, nil pointers, nil maps, and byte slices keep their existing output.