{"record":{"id":"cdac6f60876e134c","repo":"gastownhall/beads","slug":"w-metadata-edits-require-an-object-v","errorCode":null,"errorMessage":"%w: metadata edits require an object: %v","messagePattern":"%w: metadata edits require an object: (.+?)","errorType":"validation","errorClass":"storage.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/aggregate.go","lineNumber":235,"sourceCode":"\t} else {\n\t\tnext = append(json.RawMessage(nil), current...)\n\t\tif patch.Merge.Set {\n\t\t\t// A JSON null unmarshals into a nil overlay map, so the merge\n\t\t\t// below would silently accept it as \"change nothing\".\n\t\t\tif strings.TrimSpace(string(patch.Merge.Value)) == \"null\" {\n\t\t\t\treturn nil, false, fmt.Errorf(\"%w: metadata merge must be a JSON object\", storage.ErrValidation)\n\t\t\t}\n\t\t\tmerged, err := storage.MergeMetadataJSON(next, patch.Merge.Value)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, false, fmt.Errorf(\"%w: metadata merge: %v\", storage.ErrValidation, err)\n\t\t\t}\n\t\t\tnext = merged\n\t\t}\n\t\tif len(patch.Set) > 0 || len(patch.Unset) > 0 {\n\t\t\tvalues := make(map[string]json.RawMessage)\n\t\t\tif len(next) > 0 && string(next) != \"null\" {\n\t\t\t\tif err := json.Unmarshal(next, &values); err != nil {\n\t\t\t\t\treturn nil, false, fmt.Errorf(\"%w: metadata edits require an object: %v\", storage.ErrValidation, err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor _, key := range setKeys {\n\t\t\t\tvalue := patch.Set[key]\n\t\t\t\tif !json.Valid(value) {\n\t\t\t\t\treturn nil, false, fmt.Errorf(\"%w: metadata value for key %q is not valid JSON\", storage.ErrValidation, key)\n\t\t\t\t}\n\t\t\t\tvalues[key] = append(json.RawMessage(nil), value...)\n\t\t\t}\n\t\t\tfor _, key := range patch.Unset {\n\t\t\t\tdelete(values, key)\n\t\t\t}\n\t\t\tencoded, err := json.Marshal(values)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, false, fmt.Errorf(\"%w: encode metadata edits: %v\", storage.ErrValidation, err)\n\t\t\t}\n\t\t\tnext = encoded\n\t\t}","sourceCodeStart":217,"sourceCodeEnd":253,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/aggregate.go#L217-L253","documentation":"When applying patch.Set/patch.Unset edits, ApplyMetadataPatch unmarshals the (post-merge/replace) metadata into a map; if that document is non-null but not a JSON object, it wraps storage.ErrValidation with this message. Metadata key-level edits only make sense on an object, so any other shape is rejected.","triggerScenarios":"Calling ApplyMetadataPatch with Set or Unset entries while the current metadata (after Replace/Merge steps) is a JSON array, string, number, or boolean rather than an object.","commonSituations":"Issues whose metadata was previously written as a bare array or scalar by an older version or direct SQL edit; mixing Replace with a non-object value and then adding Set keys; importing metadata from external tooling.","solutions":["Replace the metadata with a valid object first (patch.Replace) instead of doing key-level edits on a non-object.","Verify current metadata shape (json.Unmarshal into map[string]json.RawMessage) before issuing a Set/Unset patch.","Fix historical data: rewrite non-object metadata blobs to objects via a migration or full replace."],"exampleFix":"// before\npatch.Replace.Set = true\npatch.Replace.Value = json.RawMessage(`[1,2]`)\npatch.Set[\"k\"] = json.RawMessage(`1`) // edits on non-object\n// after\npatch.Replace.Set = true\npatch.Replace.Value = json.RawMessage(`{}`)\npatch.Set[\"k\"] = json.RawMessage(`1`)","handlingStrategy":"validation","validationCode":"func metadataIsObject(next json.RawMessage) bool {\n    if len(next) == 0 || string(next) == \"null\" { return true }\n    var m map[string]json.RawMessage\n    return json.Unmarshal(next, &m) == nil\n}","typeGuard":null,"tryCatchPattern":"if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), \"require an object\") { /* replace metadata with an object before Set/Unset */ }","preventionTips":["Don't mix key-level Set/Unset edits with non-object metadata; Replace first.","Verify stored metadata shape before patching (unmarshal into a map).","Prevent non-object metadata from being written at all via validation.","Migrate legacy non-object blobs to objects before applying key edits."],"tags":["go","validation","metadata","json"],"backgroundTag":"metadata-not-an-object","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}