gastownhall/beads · error
metadata merge must be a JSON object
Error message
metadata merge must be a JSON object
What it means
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.
Source
Thrown at internal/storage/uow/issue_operations.go:555
func validateCloseRequest(request publicops.CloseRequest) error {
if request.Actor == "" || request.IssueID == "" {
return validationError(fmt.Errorf("close: actor and issue ID must not be empty"))
}
return nil
}
func validateMetadataPatch(metadata publicops.MetadataPatch) error {
if metadata.Replace.Set && (metadata.Merge.Set || len(metadata.Set) > 0 || len(metadata.Unset) > 0) {
return fmt.Errorf("metadata replacement cannot combine with incremental edits")
}
if metadata.Replace.Set && len(metadata.Replace.Value) > 0 && !json.Valid(metadata.Replace.Value) {
return fmt.Errorf("metadata replacement is not valid JSON")
}
if metadata.Merge.Set {
var object map[string]json.RawMessage
if len(metadata.Merge.Value) == 0 || json.Unmarshal(metadata.Merge.Value, &object) != nil || object == nil {
return fmt.Errorf("metadata merge must be a JSON object")
}
}
keys := make([]string, 0, len(metadata.Set))
for key := range metadata.Set {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
if err := storage.ValidateMetadataKey(key); err != nil {
return err
}
if !json.Valid(metadata.Set[key]) {
return fmt.Errorf("metadata value for key %q is not valid JSON", key)
}
}
for _, key := range metadata.Unset {
if err := storage.ValidateMetadataKey(key); err != nil {
return errView on GitHub (pinned to 71377f2769)
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
Example fix
// before
patch.Metadata.Merge.Set = true
patch.Metadata.Merge.Value = []byte(`["priority"]`)
// after
patch.Metadata.Merge.Set = true
patch.Metadata.Merge.Value = []byte(`{"priority":"high"}`) Defensive patterns
Strategy: validation
Validate before calling
func validMerge(v []byte) error {
if len(v) == 0 { return fmt.Errorf("merge value empty") }
var obj map[string]json.RawMessage
if json.Unmarshal(v, &obj) != nil || obj == nil { return fmt.Errorf("merge must be JSON object") }
return nil
} Type guard
func isJSONObject(v []byte) bool {
var obj map[string]json.RawMessage
return len(v) > 0 && json.Unmarshal(v, &obj) == nil && obj != nil
} Try / catch
if err := uc.Update(ctx, req); err != nil {
if errors.Is(err, publicops.ErrValidation) && strings.Contains(err.Error(), "JSON object") {
// reshape merge value into an object and retry
}
return err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- metadata replacement cannot combine with incremental edits
- metadata replacement is not valid JSON
- metadata value for key %q is not valid JSON
- no store is open for this workspace
- not found
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/47f569bec63ce2c7.
Report an issue: GitHub.