gastownhall/beads · error · storage.ErrValidation

%w: metadata merge: %v

Error message

%w: metadata merge: %v

What it means

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.

Source

Thrown at internal/storage/issueops/aggregate.go:227

	if patch.Replace.Set {
		next = append(json.RawMessage(nil), patch.Replace.Value...)
		if len(next) == 0 {
			next = json.RawMessage(`{}`)
		}
		if !json.Valid(next) {
			return nil, false, fmt.Errorf("%w: metadata replacement is not valid JSON", storage.ErrValidation)
		}
	} else {
		next = append(json.RawMessage(nil), current...)
		if patch.Merge.Set {
			// A JSON null unmarshals into a nil overlay map, so the merge
			// below would silently accept it as "change nothing".
			if strings.TrimSpace(string(patch.Merge.Value)) == "null" {
				return nil, false, fmt.Errorf("%w: metadata merge must be a JSON object", storage.ErrValidation)
			}
			merged, err := storage.MergeMetadataJSON(next, patch.Merge.Value)
			if err != nil {
				return nil, false, fmt.Errorf("%w: metadata merge: %v", storage.ErrValidation, err)
			}
			next = merged
		}
		if len(patch.Set) > 0 || len(patch.Unset) > 0 {
			values := make(map[string]json.RawMessage)
			if len(next) > 0 && string(next) != "null" {
				if err := json.Unmarshal(next, &values); err != nil {
					return nil, false, fmt.Errorf("%w: metadata edits require an object: %v", storage.ErrValidation, err)
				}
			}
			for _, key := range setKeys {
				value := patch.Set[key]
				if !json.Valid(value) {
					return nil, false, fmt.Errorf("%w: metadata value for key %q is not valid JSON", storage.ErrValidation, key)
				}
				values[key] = append(json.RawMessage(nil), value...)
			}
			for _, key := range patch.Unset {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Ensure the merge value is a valid JSON object (top-level { ... }) before calling.
  2. Validate with json.Valid and check the first non-space byte is '{' in the caller.
  3. If current metadata may be a non-object, replace it (patch.Replace) instead of merging.

Example fix

// before
patch.Merge.Value = json.RawMessage(`["a"]`) // array, not object
// after
overlay := map[string]json.RawMessage{"a": json.RawMessage(`1`)}
b, _ := json.Marshal(overlay)
patch.Merge.Set = true
patch.Merge.Value = b
Defensive patterns

Strategy: validation

Validate before calling

func validMergeJSON(v json.RawMessage) bool {
    t := strings.TrimSpace(string(v))
    return json.Valid(v) && strings.HasPrefix(t, "{")
}

Try / catch

if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), "metadata merge") { /* fix overlay shape, no retry */ }

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


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