{"record":{"id":"ab346a6f4f213274","repo":"gastownhall/beads","slug":"invalid-metadata-w-ab346a","errorCode":null,"errorMessage":"invalid metadata: %w","messagePattern":"invalid metadata: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/update.go","lineNumber":442,"sourceCode":"\tfor key, value := range updates {\n\t\tif !IsAllowedUpdateField(key) {\n\t\t\treturn nil, fmt.Errorf(\"invalid field for update: %s\", key)\n\t\t}\n\n\t\tcolumnName := key\n\t\tif key == \"wisp\" {\n\t\t\tcolumnName = \"ephemeral\"\n\t\t}\n\t\tsetClauses = append(setClauses, fmt.Sprintf(\"`%s` = ?\", columnName))\n\n\t\t// Handle JSON serialization for array fields stored as TEXT.\n\t\tif key == \"waiters\" {\n\t\t\twaitersJSON, _ := json.Marshal(value)\n\t\t\targs = append(args, string(waitersJSON))\n\t\t} else if key == \"metadata\" {\n\t\t\tmetadataStr, err := storage.NormalizeMetadataValue(value)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid metadata: %w\", err)\n\t\t\t}\n\t\t\targs = append(args, metadataStr)\n\t\t} else {\n\t\t\targs = append(args, value)\n\t\t}\n\t}\n\n\t// Auto-clear pinned column when status transitions away from \"pinned\".\n\tif rawStatus, ok := updates[\"status\"]; ok {\n\t\tvar statusStr string\n\t\tswitch v := rawStatus.(type) {\n\t\tcase string:\n\t\t\tstatusStr = v\n\t\tcase types.Status:\n\t\t\tstatusStr = string(v)\n\t\t}\n\t\tif oldIssue.Pinned && statusStr != string(types.StatusPinned) {\n\t\t\tif _, alreadySet := updates[\"pinned\"]; !alreadySet {","sourceCodeStart":424,"sourceCodeEnd":460,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/update.go#L424-L460","documentation":"The metadata value supplied to updateIssueInTx failed storage.NormalizeMetadataValue, so it cannot be serialized into the metadata column. The underlying normalization error is wrapped as \"invalid metadata: %w\".","triggerScenarios":"Passing a metadata value that is not a map[string]string (e.g. nested maps, non-string values, nil of wrong shape) or a JSON string that does not normalize cleanly when calling UpdateIssueInTx with key \"metadata\".","commonSituations":"JSON-decoded metadata containing numbers/bools instead of strings; callers passing structs rather than string maps; mixing metadata formats between versions of the schema.","solutions":["Convert metadata to map[string]string before assigning it to updates[\"metadata\"].","Inspect the wrapped error (errors.Unwrap / %w chain) for the specific normalization failure.","Marshal then unmarshal through map[string]string to coerce values: convert each value with fmt.Sprintf if needed."],"exampleFix":"// before\nupdates[\"metadata\"] = rawMetadata // map[string]interface{}\n// after\nm := map[string]string{}\nfor k, v := range rawMetadata {\n    m[k] = fmt.Sprintf(\"%v\", v)\n}\nupdates[\"metadata\"] = m","handlingStrategy":"validation","validationCode":"if raw, ok := updates[\"metadata\"]; ok {\n    if _, err := storage.NormalizeMetadataValue(raw); err != nil {\n        return fmt.Errorf(\"metadata must normalize: %w\", err)\n    }\n}","typeGuard":"func asStringMap(v interface{}) (map[string]string, bool) {\n    m, ok := v.(map[string]string)\n    return m, ok\n}","tryCatchPattern":"if _, err := storage.UpdateIssue(ctx, id, updates, actor); err != nil {\n    if strings.Contains(err.Error(), \"invalid metadata\") {\n        return fmt.Errorf(\"fix metadata shape (map[string]string): %w\", err)\n    }\n    return err\n}","preventionTips":["Always pass metadata as map[string]string.","Coerce JSON-decoded values (map[string]interface{}) to strings before assigning.","Validate metadata with NormalizeMetadataValue before the update call.","Keep metadata values small, flat, and string-typed."],"tags":["storage","validation","metadata"],"backgroundTag":"invalid-metadata","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}