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 err

View on GitHub (pinned to 71377f2769)

Solutions

  1. Ensure Merge.Value is a non-empty JSON object like {"key":"value"}
  2. Default to {} if no merge is intended, or leave Merge.Set false
  3. Unmarshal into map[string]json.RawMessage first to self-validate before the call
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/47f569bec63ce2c7. Report an issue: GitHub.