Status: Accepted Date: 2026-05-19
Every JSONL record needs a leading "type":"<discriminator>" field that the consumer relies on (see ADR-0001). We must inject this into structs whose JSON tag layout is defined elsewhere (internal/model/types.go) without:
- Modifying every record type to know about its own discriminator.
- Forcing the caller to assemble a wrapper struct per record kind.
- Forcing a
map[string]anyround-trip that loses key order and field omission semantics.
internal/model/jsonl.go's writeTyped helper marshals the payload struct into raw JSON bytes, then splices {"type":"<name>" into the front of the existing object:
raw, _ := json.Marshal(payload) // -> `{"key":"v",...}`
out := append([]byte(`{"type":`), tb...)
out = append(out, ',')
out = append(out, raw[1:]...) // payload minus its leading '{'The empty-object case ({}) is handled explicitly to avoid emitting {"type":"x",} (invalid JSON).
Upside.
- Zero impact on
model.Issue,model.Hotspot, etc. — they keep their pristine struct definitions and JSON tags. - One pass over payload bytes; no reflection beyond what
json.Marshalalready does. typeis always the first key on every line, which makesgrep -m1 '"type":"sonar.issue"'work without parsing the whole line.
Downside.
- The stitching is fragile to non-object payloads (
null, arrays, primitives). Caller contract: only structs that marshal to a JSON object. Enforced with a runtime check +fmt.Errorf. - We assume
json.Marshalproduces an object starting with'{'and ending with'}'. This is guaranteed by the encoding/json spec for non-nil struct values, but a future Go change could in principle violate it. Mitigated by aJSONLEachLineIsJSONsmoke test that re-parses every line.
- Per-type wrapper struct (
type wrappedIssue struct { Type string; ... Issue }). Multiplies the type surface; embeds make field-ordering surprises in marshal output. json.RawMessagefield on a common wrapper. Two marshals per record (inner payload, then wrapper) — measurable overhead on large reports.map[string]anyre-key. Losesomitempty, loses field order, slower.- A custom encoder. Over-engineered for one well-bounded concern.