gastownhall/beads · error
new metadata is not a JSON object: %w
Error message
new metadata is not a JSON object: %w
What it means
MergeMetadataJSON wraps the json.Unmarshal error when the incoming (overlay) metadata cannot be decoded into a JSON object map. Unlike the existing side, incoming is not given the empty/null allowance in the source shown, so any incoming value that is not a JSON object — array, scalar, quoted string, or malformed JSON — is rejected. Incoming keys overwrite existing ones, so the overlay must always be an object.
Source
Thrown at internal/storage/metadata.go:262
var jsonPathEscaper = strings.NewReplacer(`\`, `\\`, `"`, `\"`)
// MergeMetadataJSON merges incoming metadata JSON into existing metadata.
// Top-level keys from incoming overwrite keys in existing; keys only in
// existing are preserved. Both inputs must be JSON objects (or empty/null).
func MergeMetadataJSON(existing, incoming json.RawMessage) (json.RawMessage, error) {
base := make(map[string]json.RawMessage)
if len(existing) > 0 {
trimmed := strings.TrimSpace(string(existing))
if trimmed != "" && trimmed != "null" {
if err := json.Unmarshal(existing, &base); err != nil {
return nil, fmt.Errorf("existing metadata is not a JSON object: %w", err)
}
}
}
overlay := make(map[string]json.RawMessage)
if err := json.Unmarshal(incoming, &overlay); err != nil {
return nil, fmt.Errorf("new metadata is not a JSON object: %w", err)
}
for k, v := range overlay {
base[k] = v
}
result, err := json.Marshal(base)
if err != nil {
return nil, fmt.Errorf("failed to marshal merged metadata: %w", err)
}
return json.RawMessage(result), nil
}
// ApplyMetadataEdits applies incremental set (key=value) and unset (key) edits
// to existing metadata and returns the merged JSON. Set values are typed via
// MetadataEditValue; keys are validated with ValidateMetadataKey.
func ApplyMetadataEdits(existing json.RawMessage, setFlags, unsetFlags []string) (json.RawMessage, error) {
data := make(map[string]json.RawMessage)View on GitHub (pinned to 71377f2769)
Solutions
- Ensure the incoming argument is a top-level JSON object: {"key":"value"}, not ["..."] or a bare scalar.
- Check for double encoding: if the value starts with \" or is escaped JSON, decode one layer first.
- Validate with json.Unmarshal([]byte(in), &map[string]json.RawMessage{}) in a pre-check before the call.
- If passing via CLI, quote the whole JSON argument so the shell does not split or strip braces.
Example fix
// before
incoming := json.RawMessage(`"{\"k\":\"v\"}"`) // double-encoded string
merged, err := MergeMetadataJSON(existing, incoming) // fails
// after
incoming := json.RawMessage(`{"k":"v"}`)
merged, err := MergeMetadataJSON(existing, incoming) Defensive patterns
Strategy: validation
Validate before calling
func validOverlay(raw json.RawMessage) bool {
var m map[string]json.RawMessage
return len(raw) > 0 && json.Unmarshal(raw, &m) == nil
}
if !validOverlay(incoming) { /* fix the payload before calling MergeMetadataJSON */ } Type guard
func asOverlay(raw json.RawMessage) (map[string]json.RawMessage, bool) {
var m map[string]json.RawMessage
if err := json.Unmarshal(raw, &m); err != nil {
return nil, false
}
return m, true
} Try / catch
merged, err := MergeMetadataJSON(existing, incoming)
if err != nil && strings.Contains(err.Error(), "new metadata is not a JSON object") {
// decode one layer if the payload was double-encoded
var s string
if json.Unmarshal(incoming, &s) == nil {
merged, err = MergeMetadataJSON(existing, json.RawMessage(s))
}
} Prevention
- Build overlays with json.Marshal(map[string]any{...}) instead of hand-written strings.
- Beware of double-encoding: if the payload begins with a quote-wrapped document, decode once.
- Quote whole JSON arguments in shell so braces are not stripped or split.
When it happens
Trigger: Calling MergeMetadataJSON(existing, incoming) with incoming = []byte(`"string"`), an array, a number, `null` (unmarshal into map fails with cannot unmarshal), or truncated JSON. Happens when a CLI builds the overlay from --metadata flags or external JSON that was not normalized.
Common situations: Passing a JSON array of key/value pairs instead of an object; double-encoding the payload (a JSON string containing JSON); shell quoting mangling the --metadata-json argument so it arrives truncated or quoted.
Related errors
- ExternalDoltConfig: must set Socket or (Host, Port)
- metadata.repo: %w
- metadata.repo must not be null
- loading config for gate resolution: %w
- invalid metadata: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/715e764eeeec5898.
Report an issue: GitHub.