gastownhall/beads · error

existing metadata is not a JSON object: %w

Error message

existing metadata is not a JSON object: %w

What it means

During typed metadata edits, the issue's existing metadata could not be unmarshaled into a JSON object (map[string]json.RawMessage). The stored value exists but is not an object — e.g. an array, string, or number — so incremental edits cannot be applied to it.

Source

Thrown at internal/storage/issueops/update.go:931

		}
		current = merged
	}
	// Validate the merged result, matching the schema check stores apply to
	// direct metadata replacements (GH#1416 Phase 2).
	if err := ValidateMetadataIfConfigured(current); err != nil {
		return err
	}
	resolved["metadata"] = current
	return nil
}

func applyTypedMetadataEdits(existing json.RawMessage, set map[string]json.RawMessage, unset []string) (json.RawMessage, error) {
	data := make(map[string]json.RawMessage)
	if len(existing) > 0 {
		trimmed := strings.TrimSpace(string(existing))
		if trimmed != "" && trimmed != "null" {
			if err := json.Unmarshal(existing, &data); err != nil {
				return nil, fmt.Errorf("existing metadata is not a JSON object: %w", err)
			}
		}
	}
	keys := make([]string, 0, len(set))
	for key := range set {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	for _, key := range keys {
		if err := storage.ValidateMetadataKey(key); err != nil {
			return nil, err
		}
		if !json.Valid(set[key]) {
			return nil, fmt.Errorf("metadata value for key %q is not valid JSON", key)
		}
		data[key] = set[key]
	}
	for _, key := range unset {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Replace the metadata wholesale with a valid object using a direct metadata set (no incremental ops), then apply edits
  2. Migrate the offending rows: read the current value, convert it to an object, write it back
  3. Add a pre-flight check that json.Unmarshal(existing, &map[string]any{}) succeeds before doing incremental edits

Example fix

// before
// existing metadata: ["a","b"] -> edit fails

// after
// one-time migration: set metadata to {"items": ["a","b"]} via full replacement,
// then run incremental set/unset ops against the object
Defensive patterns

Strategy: type-guard

Validate before calling

var probe map[string]json.RawMessage
if len(issue.Metadata) > 0 && string(bytes.TrimSpace(issue.Metadata)) != "null" {
    if err := json.Unmarshal(issue.Metadata, &probe); err != nil {
        return errors.New("metadata must be replaced with a JSON object before incremental edits")
    }
}

Type guard

func isMetadataObject(raw json.RawMessage) bool {
    trimmed := strings.TrimSpace(string(raw))
    if trimmed == "" || trimmed == "null" { return true }
    var m map[string]json.RawMessage
    return json.Unmarshal([]byte(trimmed), &m) == nil
}

Try / catch

if err := issueops.ResolveMergeOps(issue, updates, resolved); err != nil {
    if strings.Contains(err.Error(), "existing metadata is not a JSON object") {
        // perform full replacement with a valid object, then re-apply edits
    }
}

Prevention

When it happens

Trigger: applyTypedMetadataEdits is called (via resolveMetadataMergeOps -> set/unset metadata ops) while the issue's stored metadata column holds a non-object JSON document.

Common situations: Legacy data written before object-only metadata was enforced, imports from external tools storing arrays, or manual DB edits that corrupted the metadata value.

Related errors


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