{"record":{"id":"2ce10fcfd2e174e6","repo":"gastownhall/beads","slug":"w-metadata-merge-v","errorCode":null,"errorMessage":"%w: metadata merge: %v","messagePattern":"%w: metadata merge: (.+?)","errorType":"validation","errorClass":"storage.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/aggregate.go","lineNumber":227,"sourceCode":"\tif patch.Replace.Set {\n\t\tnext = append(json.RawMessage(nil), patch.Replace.Value...)\n\t\tif len(next) == 0 {\n\t\t\tnext = json.RawMessage(`{}`)\n\t\t}\n\t\tif !json.Valid(next) {\n\t\t\treturn nil, false, fmt.Errorf(\"%w: metadata replacement is not valid JSON\", storage.ErrValidation)\n\t\t}\n\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 {","sourceCodeStart":209,"sourceCodeEnd":245,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/aggregate.go#L209-L245","documentation":"ApplyMetadataPatch wraps storage.ErrValidation with the underlying error from storage.MergeMetadataJSON when the merge overlay cannot be applied to the current metadata — typically because the overlay (or current document) is not a JSON object or is malformed. The merge is aborted and the update rejected.","triggerScenarios":"Calling ApplyMetadataPatch with patch.Merge.Set=true and a Merge.Value that MergeMetadataJSON cannot merge (non-object JSON such as an array/string/number, or structurally invalid JSON).","commonSituations":"Passing a JSON array where an object is required; merging into metadata that was previously written as a non-object; user input like `--merge '[1,2]'`; version drift where old issues hold non-object metadata.","solutions":["Ensure the merge value is a valid JSON object (top-level { ... }) before calling.","Validate with json.Valid and check the first non-space byte is '{' in the caller.","If current metadata may be a non-object, replace it (patch.Replace) instead of merging."],"exampleFix":"// before\npatch.Merge.Value = json.RawMessage(`[\"a\"]`) // array, not object\n// after\noverlay := map[string]json.RawMessage{\"a\": json.RawMessage(`1`)}\nb, _ := json.Marshal(overlay)\npatch.Merge.Set = true\npatch.Merge.Value = b","handlingStrategy":"validation","validationCode":"func validMergeJSON(v json.RawMessage) bool {\n    t := strings.TrimSpace(string(v))\n    return json.Valid(v) && strings.HasPrefix(t, \"{\")\n}","typeGuard":null,"tryCatchPattern":"if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), \"metadata merge\") { /* fix overlay shape, no retry */ }","preventionTips":["Ensure merge overlays are top-level JSON objects ({...}), never arrays/scalars.","Pre-validate with json.Valid plus a '{' prefix check.","If existing metadata may be a non-object, use Replace instead of Merge.","Test merge logic against the actual stored metadata shape."],"tags":["go","validation","metadata","json"],"backgroundTag":"metadata-merge-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}