Skip to content

fix(gen): emit omitzero for optional object request fields#27

Merged
ysyneu merged 1 commit into
mainfrom
audit-fix/2026-07-09-p2-omitzero
Jul 10, 2026
Merged

fix(gen): emit omitzero for optional object request fields#27
ysyneu merged 1 commit into
mainfrom
audit-fix/2026-07-09-p2-omitzero

Conversation

@ysyneu

@ysyneu ysyneu commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Root cause

The generator emitted every optional request field with json:"...,omitempty". stdlib encoding/json's omitempty never drops a bare struct value (only false/0/""/nil slice/map/pointer count as "empty"), so every optional object-typed request field was always transmitted as {}.

Confirmed production impact (AI-SRE audit 2026-07-09, session sess_6fnryva2E5uPEtMwJWKvEw): CreateSilenceRuleRequest.TimeFilter always rode the wire as "time_filter":{}, and the server binding validates StartTime/EndTime with gt=0 whenever time_filter is present — making a recurring-only silence rule (time_filters only) impossible via this SDK and everything built on it (fduty CLI). Six create attempts in one prod session all failed with StartTime must be greater than 0, EndTime must be greater than 0; combining both fields fails "cannot configure both".

Fix

internal/cmd/gen: for request-context fields whose schema resolves (through $ref/allOf, mirroring emitModels' classifier) to a generated struct type, emit json:"...,omitzero" (Go 1.24 stdlib; go.mod already declares go 1.24). Everything else keeps omitempty exactly as before; the mirrored toon tag stays omitempty (toon-go only recognizes that literal option). No pointer-wrapping — zero call-site breakage.

Regenerated models_gen.go: 50 fields, strictly json-tag-only diff (verified: no other hunk kinds), including Create/UpdateSilenceRuleRequest.TimeFilter. Regeneration is idempotent against the committed generator.

Verification

  • go build ./..., go vet ./..., go test ./... -count=1 green.
  • New regression test TestOptionalObjectRequestFieldOmitsWhenUnset exercises the real newRequest marshal path: unset TimeFilter produces no time_filter key; set TimeFilter keeps it. Hand-reverting the tag to omitempty reproduces the exact prod symptom ("time_filter":{} on the wire) and fails the test.
  • SDK's own marshal path (flashduty.go json.Marshal) is unconditionally stdlib; checked in-org importers (flashduty-cli, flashduty-mcp-server, terraform-provider) — none marshal these structs via jsoniter.

Follow-up (not in this PR)

flashduty-cli needs a go-flashduty bump + release after this merges so prod runners pick up the fix.

stdlib encoding/json's `,omitempty` never drops a bare struct value (only
false/0/""/nil-slice/nil-map/nil-pointer count as "empty"), so every
optional object-typed request field was always sent on the wire as `{}`.
For CreateSilenceRuleRequest.TimeFilter this made creating a
recurring-only silence rule impossible: the server's binding validates
StartTime/EndTime with `gt=0` whenever "time_filter" is present at all.

Emit `,omitzero` (Go 1.24+) instead of `,omitempty` in the json tag for
request fields that resolve to a generated struct type, leaving the
mirrored `toon` tag on `,omitempty` (toon-go only recognizes that
literal option) and all slice/map/scalar/pointer field handling
untouched. Regenerated models_gen.go: 50 fields affected, json-tag-only
diff.
@ysyneu ysyneu merged commit 15a3153 into main Jul 10, 2026
4 checks passed
@ysyneu ysyneu deleted the audit-fix/2026-07-09-p2-omitzero branch July 10, 2026 04:16
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.

1 participant