{"record":{"id":"e927c23435dd6c91","repo":"gastownhall/beads","slug":"journal-marshal-issue-s-w","errorCode":null,"errorMessage":"journal: marshal issue %s: %w","messagePattern":"journal: marshal issue (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/journal.go","lineNumber":425,"sourceCode":"\t\t}\n\t\tissue.IsBlocked = blocked != 0\n\t\treturn issue, nil\n\t}\n\treturn nil, fmt.Errorf(\"%w: issue %s\", storage.ErrNotFound, issueID)\n}\n\n// insertEventRow performs the actual INSERT. It is the ONE seam both write\n// plumbings funnel through, so the seq mechanism cannot drift between them. A\n// nil issue is stored as SQL NULL (deletes); a nil dep is stored as SQL NULL\n// (non-dependency ops). ts is the insert time, stamped inside the committing\n// transaction. actor is stored as-is — \"\" for the genuinely unattributable\n// paths — in the NOT NULL DEFAULT ” actor column.\nfunc insertEventRow(ctx context.Context, tx DBTX, op EventOp, issueID string, issue *types.Issue, dep *EventDep, comment *EventComment, actor string) error {\n\tvar issueJSON any\n\tif issue != nil {\n\t\tb, err := json.Marshal(issue)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"journal: marshal issue %s: %w\", issueID, err)\n\t\t}\n\t\tissueJSON = string(b)\n\t}\n\tvar depJSON any\n\tif dep != nil {\n\t\tb, err := json.Marshal(dep)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"journal: marshal dep for %s: %w\", issueID, err)\n\t\t}\n\t\tdepJSON = string(b)\n\t}\n\tvar commentJSON any\n\tif comment != nil {\n\t\tb, err := json.Marshal(comment)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"journal: marshal comment for %s: %w\", issueID, err)\n\t\t}\n\t\tcommentJSON = string(b)","sourceCodeStart":407,"sourceCodeEnd":443,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/journal.go#L407-L443","documentation":"insertEventRow marshals the issue snapshot to JSON before inserting the journal row; if json.Marshal fails the error is wrapped as 'journal: marshal issue'. Since types.Issue is JSON-tag-annotated this should be nearly impossible, but it surfaces invalid field types (e.g. non-UTF8, unsupported types injected via custom fields).","triggerScenarios":"RecordEventInTx/RecordDeleteInTx/RecordDepEventInTx/RecordCommentEventInTx called with an *types.Issue whose contents cannot be marshaled — typically custom metadata holding unmarshalable values or corrupted in-memory state.","commonSituations":"Custom importers stuffing raw map/func values into issue metadata structs; version mismatch where a field type changed and holds incompatible data; fixtures with cyclic references.","solutions":["Inspect the wrapped %w error to find the offending field","Sanitize issue metadata to JSON-serializable types before calling journal APIs","Upgrade/downgrade so the in-process types.Issue matches the persisted data","Report upstream if stock types.Issue fails to marshal (likely a bug)"],"exampleFix":"// before\nissue.Metadata[\"cb\"] = func() {} // unmarshalable\nops.RecordEventInTx(ctx, tx, op, id, issue, nil, nil)\n// after\nissue.Metadata[\"cb\"] = nil // strip unsupported values before journaling\nops.RecordEventInTx(ctx, tx, op, id, issue, nil, nil)","handlingStrategy":"try-catch","validationCode":"if _, err := json.Marshal(issue); err != nil {\n    return fmt.Errorf(\"issue %s not JSON-serializable: %w\", issue.ID, err)\n}","typeGuard":null,"tryCatchPattern":"if err := ops.RecordEventInTx(ctx, tx, op, id, issue, nil, nil); err != nil {\n    if strings.Contains(err.Error(), \"marshal issue\") {\n        return fmt.Errorf(\"bad issue payload: %w\", err)\n    }\n    return err\n}","preventionTips":["Keep issue metadata to JSON-native types (string, number, bool, map, slice)","Test-marshall payloads in importers before writing","Avoid injecting functions/cycles into types.Issue fields"],"tags":["journal","json","serialization"],"backgroundTag":"json-marshal-failure","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}