Uh oh!
There was an error while loading. Please reload this page.
fix: render unset timestamps as null in --json output - #170
Merged
Conversation
go-flashduty's Timestamp.MarshalJSON emits a quoted RFC3339 string for a set value but the bare integer 0 for the unset sentinel, so one field switches JSON type depending on record state: an active alert's end_time is the number 0 while a recovered alert's is a string. jq arithmetic over mixed-state output fails with "string and number cannot be subtracted". Add output.NullUnsetInstants, a reflection-based transform applied at the JSON marshaling boundary (JSONPrinter.Print and marshalStructured), that replaces every unset SDK timestamp with nil so it renders as null. Set values keep their concrete type and custom MarshalJSON, non-timestamp zero integers are untouched, and struct field order is preserved (verified byte-identical when no unset instant is present). TOON and table output are unchanged: they render the sentinel through String(), already always a string. The generated help note and the skill card prose that documented the old "unset stays the bare integer 0" contract are updated to match.
writeSessionList marshals SessionItem structs directly, bypassing both patched boundaries (JSONPrinter.Print and marshalStructured), so session list --output-format json/jsonl still emitted "archived_at": 0 for live (not archived) sessions while the generated help note already claimed unset values render as null. Route both the json envelope and the per-line jsonl encode through output.NullUnsetInstants, and add a regression test covering unset archived_at -> null on both paths. Also collapse the instantLike interface added in instant.go into the output package's existing instant interface (table.go): the Unix method was redundant since Time() already excludes time.Time.
Uh oh!
There was an error while loading. Please reload this page.
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 freeto 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.
Problem
In
--jsonoutput, timestamp fields have an unstable type: a set value renders as an RFC3339 string, but an unset value renders as the bare integer0. Foralert list/alert getthis meansend_timeis a string for recovered alerts and a number for active ones, so jq arithmetic over mixed-state results fails (string and number cannot be subtracted).Root cause
The CLI passes typed go-flashduty structs straight to
json.MarshalIndent. The SDK'sTimestamp.MarshalJSON(go-flashdutytimestamp.go) emits a quoted RFC3339 string for a set value and the bare integer0for the unset sentinel — an intentional SDK-side round-trip contract. The wire format itself is consistent (always integer Unix seconds), so this is an output-rendering concern, and output formatting is the CLI's layer.Fix
New
output.NullUnsetInstants: a reflection-based transform applied at the CLI's JSON marshaling boundary (JSONPrinter.PrintandmarshalStructured). It rebuilds only subtrees containing SDK timestamps (reflect.StructOfover identical field names/tags/order) and replaces each unset timestamp withnil, so--jsonnow renders:MarshalJSONpreserved)null(was: bare0)0(untouched)Struct field order and byte shape are preserved when no unset instant is present (covered by a byte-identity test). TOON and table output are unchanged: both render the sentinel through
String(), already always a string.Docs that described the old contract are updated: the generated
--helpnote (cligentimestampOverrideNote+ regeneratedzz_generated_*, note-phrase-only changes) andskills/flashduty/SKILL.md.Verification
make check(fmt + golangci-lint +go test -race ./...+ build) — all green.internal/output/instant_test.go: zero→null, set→RFC3339,TimestampMilli, slices/pointers/projectedmap[string]anyrows, non-timestamp0untouched, byte-identical field order.alert list --json→"end_time": null/"2026-08-26T20:16:08-07:00"alert get --jsonand--fields alert_id,end_timeprojection → same