gastownhall/beads · error · storage.ErrValidation

%w: metadata edits require an object: %v

Error message

%w: metadata edits require an object: %v

What it means

When applying patch.Set/patch.Unset edits, ApplyMetadataPatch unmarshals the (post-merge/replace) metadata into a map; if that document is non-null but not a JSON object, it wraps storage.ErrValidation with this message. Metadata key-level edits only make sense on an object, so any other shape is rejected.

Source

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

	} 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 {
				delete(values, key)
			}
			encoded, err := json.Marshal(values)
			if err != nil {
				return nil, false, fmt.Errorf("%w: encode metadata edits: %v", storage.ErrValidation, err)
			}
			next = encoded
		}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Replace the metadata with a valid object first (patch.Replace) instead of doing key-level edits on a non-object.
  2. Verify current metadata shape (json.Unmarshal into map[string]json.RawMessage) before issuing a Set/Unset patch.
  3. Fix historical data: rewrite non-object metadata blobs to objects via a migration or full replace.

Example fix

// before
patch.Replace.Set = true
patch.Replace.Value = json.RawMessage(`[1,2]`)
patch.Set["k"] = json.RawMessage(`1`) // edits on non-object
// after
patch.Replace.Set = true
patch.Replace.Value = json.RawMessage(`{}`)
patch.Set["k"] = json.RawMessage(`1`)
Defensive patterns

Strategy: validation

Validate before calling

func metadataIsObject(next json.RawMessage) bool {
    if len(next) == 0 || string(next) == "null" { return true }
    var m map[string]json.RawMessage
    return json.Unmarshal(next, &m) == nil
}

Try / catch

if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), "require an object") { /* replace metadata with an object before Set/Unset */ }

Prevention

When it happens

Trigger: Calling ApplyMetadataPatch with Set or Unset entries while the current metadata (after Replace/Merge steps) is a JSON array, string, number, or boolean rather than an object.

Common situations: Issues whose metadata was previously written as a bare array or scalar by an older version or direct SQL edit; mixing Replace with a non-object value and then adding Set keys; importing metadata from external tooling.

Related errors


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