{"record":{"id":"47f569bec63ce2c7","repo":"gastownhall/beads","slug":"metadata-merge-must-be-a-json-object","errorCode":null,"errorMessage":"metadata merge must be a JSON object","messagePattern":"metadata merge must be a JSON object","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/uow/issue_operations.go","lineNumber":555,"sourceCode":"\nfunc validateCloseRequest(request publicops.CloseRequest) error {\n\tif request.Actor == \"\" || request.IssueID == \"\" {\n\t\treturn validationError(fmt.Errorf(\"close: actor and issue ID must not be empty\"))\n\t}\n\treturn nil\n}\n\nfunc validateMetadataPatch(metadata publicops.MetadataPatch) error {\n\tif metadata.Replace.Set && (metadata.Merge.Set || len(metadata.Set) > 0 || len(metadata.Unset) > 0) {\n\t\treturn fmt.Errorf(\"metadata replacement cannot combine with incremental edits\")\n\t}\n\tif metadata.Replace.Set && len(metadata.Replace.Value) > 0 && !json.Valid(metadata.Replace.Value) {\n\t\treturn fmt.Errorf(\"metadata replacement is not valid JSON\")\n\t}\n\tif metadata.Merge.Set {\n\t\tvar object map[string]json.RawMessage\n\t\tif len(metadata.Merge.Value) == 0 || json.Unmarshal(metadata.Merge.Value, &object) != nil || object == nil {\n\t\t\treturn fmt.Errorf(\"metadata merge must be a JSON object\")\n\t\t}\n\t}\n\tkeys := make([]string, 0, len(metadata.Set))\n\tfor key := range metadata.Set {\n\t\tkeys = append(keys, key)\n\t}\n\tsort.Strings(keys)\n\tfor _, key := range keys {\n\t\tif err := storage.ValidateMetadataKey(key); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !json.Valid(metadata.Set[key]) {\n\t\t\treturn fmt.Errorf(\"metadata value for key %q is not valid JSON\", key)\n\t\t}\n\t}\n\tfor _, key := range metadata.Unset {\n\t\tif err := storage.ValidateMetadataKey(key); err != nil {\n\t\t\treturn err","sourceCodeStart":537,"sourceCodeEnd":573,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/uow/issue_operations.go#L537-L573","documentation":"A Merge patch must be a JSON object; validateMetadataPatch unmarshals Merge.Value into map[string]json.RawMessage and rejects empty values, non-object JSON (arrays, scalars), or nil results with this error.","triggerScenarios":"Calling Update with Patch.Metadata.Merge.Set = true where Merge.Value is empty, is not valid JSON, or parses to a non-object (e.g. `[1,2]` or `\"text\"`) or to null.","commonSituations":"Passing a JSON array where key/value metadata was expected, an empty byte slice from an unset variable, or a single quoted string instead of an object with keys.","solutions":["Ensure Merge.Value is a non-empty JSON object like {\"key\":\"value\"}","Default to {} if no merge is intended, or leave Merge.Set false","Unmarshal into map[string]json.RawMessage first to self-validate before the call","Check input producers (flags, files) for arrays/scalars leaking into merge"],"exampleFix":"// before\npatch.Metadata.Merge.Set = true\npatch.Metadata.Merge.Value = []byte(`[\"priority\"]`)\n// after\npatch.Metadata.Merge.Set = true\npatch.Metadata.Merge.Value = []byte(`{\"priority\":\"high\"}`)","handlingStrategy":"validation","validationCode":"func validMerge(v []byte) error {\n    if len(v) == 0 { return fmt.Errorf(\"merge value empty\") }\n    var obj map[string]json.RawMessage\n    if json.Unmarshal(v, &obj) != nil || obj == nil { return fmt.Errorf(\"merge must be JSON object\") }\n    return nil\n}","typeGuard":"func isJSONObject(v []byte) bool {\n    var obj map[string]json.RawMessage\n    return len(v) > 0 && json.Unmarshal(v, &obj) == nil && obj != nil\n}","tryCatchPattern":"if err := uc.Update(ctx, req); err != nil {\n    if errors.Is(err, publicops.ErrValidation) && strings.Contains(err.Error(), \"JSON object\") {\n        // reshape merge value into an object and retry\n    }\n    return err\n}","preventionTips":["Replicate the library's map[string]json.RawMessage check before calling","Never pass arrays or scalars as merge payloads","Guard empty byte slices from unset variables with a default of {}","Use json.Marshal(map[string]any{...}) to construct merge values"],"tags":["validation","metadata-patch","json-object","go"],"backgroundTag":"invalid-json","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}