{"record":{"id":"3efc9afafc70c155","repo":"Billionmail/BillionMail","slug":"parse-contact-attribs-w","errorCode":null,"errorMessage":"parse contact attribs: %w","messagePattern":"parse contact attribs: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/video_gen/orchestrator.go","lineNumber":337,"sourceCode":"\tupdateJobStatus(ctx, jobID, JobFailed, phase, err.Error())\n}\n\n// loadContactAttribs fetches attribs for a contact from DB.\nfunc loadContactAttribs(ctx context.Context, contactID, groupID int) (map[string]string, error) {\n\tval, err := g.DB().Model(\"bm_contacts\").Ctx(ctx).\n\t\tWhere(\"id\", contactID).\n\t\tWhere(\"group_id\", groupID).\n\t\tValue(\"attribs\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"load contact attribs: %w\", err)\n\t}\n\tif val.IsNil() || val.IsEmpty() {\n\t\treturn nil, fmt.Errorf(\"contact %d has no attribs\", contactID)\n\t}\n\n\tresult := make(map[string]string)\n\tif err := val.Scan(&result); err != nil {\n\t\treturn nil, fmt.Errorf(\"parse contact attribs: %w\", err)\n\t}\n\treturn result, nil\n}\n\n// updateContactVideoAttribs merges video URLs into Contact.Attribs.\nfunc updateContactVideoAttribs(ctx context.Context, contactID, groupID int, videoURL, thumbURL, landingURL string) error {\n\t// Load existing attribs\n\tattribs, err := loadContactAttribs(ctx, contactID, groupID)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tattribs[\"video_url\"] = videoURL\n\tattribs[\"thumbnail_url\"] = thumbURL\n\tattribs[\"landing_page_url\"] = landingURL\n\n\t_, err = g.DB().Model(\"bm_contacts\").Ctx(ctx).\n\t\tWhere(\"id\", contactID).","sourceCodeStart":319,"sourceCodeEnd":355,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/video_gen/orchestrator.go#L319-L355","documentation":"loadContactAttribs calls val.Scan(&result) to deserialize the attribs database value into map[string]string. This error wraps whatever Scan returns when the stored value cannot be decoded into that map — typically malformed JSON, or a JSON object whose values are not strings (numbers, booleans, nested objects). It indicates the attribs payload is present but not shaped as map[string]string.","triggerScenarios":"attribs column contains invalid JSON (truncated write, plain text instead of JSON); attribs is a JSON object with non-string values, e.g. {\"email_count\": 5} or nested objects like {\"signals\": {\"seo\": true}} which cannot Scan into map[string]string.","commonSituations":"Import tools writing raw CSV cell text into attribs; enrichment code storing numeric or nested values instead of stringified ones; manual SQL edits corrupting the JSON; older schema where attribs was plain text and rows were never migrated.","solutions":["Inspect the raw attribs value for the failing contact and validate it as JSON (e.g. SELECT attribs::jsonb ...)","Ensure all enrichment/import writers store only string values (convert numbers/bools with fmt.Sprintf or strconv)","Flatten nested JSON before writing, or change the Scan target to map[string]interface{} and coerce types manually","Add a data migration that normalizes or clears malformed attribs rows"],"exampleFix":"// before\nattribs[\"email_count\"] = 5 // stored as JSON number, breaks map[string]string scan\n// after\nattribs[\"email_count\"] = \"5\" // store all values as strings","handlingStrategy":"validation","validationCode":"var probe map[string]interface{}\nif err := json.Unmarshal([]byte(rawAttribs), &probe); err != nil {\n    return fmt.Errorf(\"contact %d has malformed attribs JSON: %w\", contactID, err)\n}\nfor k, v := range probe {\n    if _, ok := v.(string); !ok {\n        return fmt.Errorf(\"attribs key %q is not a string (got %T)\", k, v)\n    }\n}","typeGuard":"func isStringMap(m map[string]interface{}) bool {\n    for _, v := range m {\n        if _, ok := v.(string); !ok {\n            return false\n        }\n    }\n    return true\n}","tryCatchPattern":"result, err := loadContactAttribs(ctx, contactID, groupID)\nif err != nil {\n    if strings.HasPrefix(err.Error(), \"parse contact attribs:\") {\n        // corrupt JSON: quarantine the record for manual/automated repair\n        quarantineAttribs(contactID, groupID, err)\n        return err\n    }\n    return err\n}","preventionTips":["Store attribs as JSONB and marshal through a typed struct so writers can't emit non-string values","Always json.Marshal maps before persisting; never interpolate raw strings","Add a periodic job validating attribs parses as JSON across bm_contacts","Centralize attribs writes in one service function instead of scattered SQL updates"],"tags":["json","parsing","data-quality","goframe"],"backgroundTag":"json-parse-error","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}