Go: return an empty list, not nil, when a listing has no items

Merged
JE
2d ago
+218-53

Problem

Go service-layer list methods built their result with var xs []T and appended per item, so a listing with no items returned a nil slice. A nil slice encodes as JSON null, so consumers that serialize SDK results reported an empty listing as null rather than []. Observed in production through basecamp-mcp-server: list_folders on a project with no folders answered with the bare text null (and no structured result).

Change

  • Every listing that decodes a top-level array now starts from an empty slice (xs := []T{}), across 31 service files (53 sites). Behavior is otherwise unchanged; the populated path appends exactly as before.
  • list_results_empty_test.go drives every AccountClient service method that returns a list (a slice, or a pointer to a struct carrying one) against a server that answers [], and asserts the list is non-nil. It enumerates by reflection, so a list method added later is covered without anyone naming it. A floor (50; 70 are exercised today) fails the test if the harness stops reaching them. Against main the test reports 62 nil lists; with this change, none.

Not changed, deliberately

  • Nested model fields. Those that distinguish absent from present-but-empty (Todo.CompletionSubscribers) or omit when empty (Todo.Assignees, omitempty) do so on purpose.
  • Request/option structs (BucketIDs, Types, Days, ...): turning a nil into [] there would change request bodies.
  • Listings that json.Unmarshal the body directly (gauges, my assignments, notifications, everything files): [] already decodes to an empty, non-nil slice.
  • Other SDKs: the nil-vs-empty distinction is Go's; the other runtimes decode [] to an empty array.

No spec, OpenAPI or generated-code change. make go-check go-check-drift go-check-generated-drift go-check-wrapper-drift passes.

Consumer: basecamp/basecamp-mcp-server will bump to the release carrying this.

Overlap

#991 (fix-pass-api-semantics) touches go/pkg/basecamp/cards.go, checkins.go, lineup.go and projects.go too. Both are rebased on main independently and merge with each other without a textual conflict; the merged tree passes go build, go vet and go test ./.... Whichever lands second needs a small rebase onto the new main.


Summary by cubic

Fixes the Go SDK's list methods returning a nil slice — which encodes as JSON null — when a listing has no items, so empty listings now serialize as [] instead of null. This was observed in production through basecamp-mcp-server, where list_folders on a project with no folders answered bare null.

  • Changes 53 sites across 31 service files from var xs []T to xs := []T{}.
  • Adds a reflective test that drives every AccountClient service method returning a list against a server answering [], asserting the result is non-nil; a floor keeps the harness honest as new list methods are added.
  • Deliberately unchanged: nested model fields that distinguish absent from present-but-empty, and request/option structs.
  • No spec, OpenAPI, or generated-code changes. basecamp/basecamp-mcp-server will bump to the release carrying this.