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
- Replace the metadata wholesale with a valid object using a direct metadata set (no incremental ops), then apply edits
- Migrate the offending rows: read the current value, convert it to an object, write it back
- 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
- Run a migration converting legacy non-object metadata to objects
- Reject non-object metadata at ingestion/import time
- Use isMetadataObject as a gate before any incremental metadata op
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
- ExternalDoltConfig: must set Socket or (Host, Port)
- metadata must be a JSON object: %w
- metadata.repo: %w
- metadata.repo must not be null
- metadata.repo must be a string, got %T
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/17ba40584dc45f1a.
Report an issue: GitHub.