Skip to content

feat(output): emit human-readable status string in JSON#107

Merged
ryanlewis merged 3 commits into
mainfrom
json-status-enum
Jun 22, 2026
Merged

feat(output): emit human-readable status string in JSON#107
ryanlewis merged 3 commits into
mainfrom
json-status-enum

Conversation

@ryanlewis

Copy link
Copy Markdown
Owner

Closes #105.

What

JSON output now renders task/project/checklist status as a string enum instead of the raw Things integer:

"status": "completed"   // was: "status": 3

The non-contiguous ints (0 open, 2 cancelled, 3 completed — no 1) were pure implementation detail and easy to misread — a bare 3 reads as a rank, not "done". This applies to Task, Project, and ChecklistItem, which all shared the raw-int field.

How

  • New model.Status named int type. The existing StatusOpen/StatusCancelled/StatusCompleted constants are now typed Status, so every comparison in output/ and db/ kept working untouched.
  • MarshalJSON → string name for recognized codes. Plain-text rendering is unchanged (still capitalized Open/Cancelled/Completed); only the JSON path moved to lowercase.
  • UnmarshalJSON accepts both the string name and the raw integer, so emitted JSON round-trips and legacy integer input still parses.

Codec hardening (post-review)

A workflow-backed code review surfaced a few rough edges in the codec, fixed in the second commit:

  • Lossless round-trip — an unrecognized raw code now marshals as its integer rather than a lossy "unknown" string that UnmarshalJSON rejected. Known statuses (the only ones that occur in real data) still always emit clean strings.
  • Honest errorsUnmarshalJSON dispatches on the JSON token type, so a malformed string token reports a string error instead of silently falling through to the integer branch with a misleading int-parse error.
  • Single source of truthString/MarshalJSON/UnmarshalJSON share one statusNames map instead of three switches that could drift.

Compatibility

Breaking change to the --json status field for any consumer that read it as a number. Acceptable at 0.x and the explicit ask in #105.

Tests

Added TestStatusMarshalJSON, TestStatusUnmarshalJSON (string + legacy int + unknown-code round-trip + malformed-token errors), and a round-trip test covering both recognized and unrecognized codes. make lint clean, 256 tests pass.

JSON output now renders task/project/checklist status as a string enum
("open" | "cancelled" | "completed") instead of the raw Things
integer (0/2/3). The non-contiguous ints were pure implementation detail
and easy to misread — a bare 3 reads as a rank, not "done".

Introduces a model.Status named type with String/MarshalJSON, plus an
UnmarshalJSON that accepts both the string name and the legacy integer so
existing JSON round-trips and integer input still parse.

Closes #105
Address code-review findings on the Status codec:

- Single source of truth: String/MarshalJSON/UnmarshalJSON now share one
  statusNames map instead of three hand-maintained switches that could drift.
- Lossless round-trip: an unrecognized raw Things code now marshals as its
  integer instead of a lossy "unknown" string that UnmarshalJSON rejected.
- Honest errors: UnmarshalJSON dispatches on the JSON token type, so a
  malformed string token reports a string error rather than silently falling
  through to the integer branch and surfacing a misleading int-parse error.
Address a second code-review pass on the Status codec:

- Correctness: UnmarshalJSON silently coerced a JSON null into Status(0)
  ("open"), clobbering a pre-set status. Per the json.Unmarshaler
  convention, null is now a no-op that leaves the existing value untouched.
- Reuse: replace the manual bytes.TrimSpace + leading-quote sniff (and its
  sole-purpose bytes import) with the stdlib idiom — try the string name,
  fall back to the raw integer on a *json.UnmarshalTypeError, surface other
  errors as-is. Same accept-string-or-legacy-int behavior, less hand-rolled.
- Convention: document the JSON status string enum in internal/skill/SKILL.md
  (CLAUDE.md requires SKILL.md track output-surface changes).
- Tests: add the legacy cancelled (2) integer-decode case, a null no-op
  case, and StatusCancelled to the round-trip.
@ryanlewis ryanlewis merged commit 8656bd8 into main Jun 22, 2026
1 check passed
@ryanlewis ryanlewis deleted the json-status-enum branch June 22, 2026 22:25
ryanlewis added a commit that referenced this pull request Jun 22, 2026
Release-notes header for the upcoming `v0.5.0` tag (required at the
tagged commit by `.goreleaser.yaml`).

Covers the two behaviour changes in this release:
- JSON `status` now emits a string instead of the raw int (#107, closes
#105)
- `things today` excludes completed by default; `--include-completed`
restores UI-parity (#109, closes #106)

Plus dependency maintenance (#100, #108).

Part of cutting v0.5.0 via `/release minor`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JSON output exposes raw integer status (0/2/3) instead of a human-readable value

1 participant