feat(output): emit human-readable status string in JSON#107
Merged
Conversation
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
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`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #105.
What
JSON output now renders task/project/checklist
statusas a string enum instead of the raw Things integer:The non-contiguous ints (
0open,2cancelled,3completed — no1) were pure implementation detail and easy to misread — a bare3reads as a rank, not "done". This applies toTask,Project, andChecklistItem, which all shared the raw-int field.How
model.Statusnamed int type. The existingStatusOpen/StatusCancelled/StatusCompletedconstants are now typedStatus, so every comparison inoutput/anddb/kept working untouched.MarshalJSON→ string name for recognized codes. Plain-text rendering is unchanged (still capitalizedOpen/Cancelled/Completed); only the JSON path moved to lowercase.UnmarshalJSONaccepts 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:
"unknown"string thatUnmarshalJSONrejected. Known statuses (the only ones that occur in real data) still always emit clean strings.UnmarshalJSONdispatches 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.String/MarshalJSON/UnmarshalJSONshare onestatusNamesmap instead of three switches that could drift.Compatibility
Breaking change to the
--jsonstatusfield 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 lintclean, 256 tests pass.