{"record":{"id":"715e764eeeec5898","repo":"gastownhall/beads","slug":"new-metadata-is-not-a-json-object-w","errorCode":null,"errorMessage":"new metadata is not a JSON object: %w","messagePattern":"new metadata is not a JSON object: %w","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/metadata.go","lineNumber":262,"sourceCode":"var jsonPathEscaper = strings.NewReplacer(`\\`, `\\\\`, `\"`, `\\\"`)\n\n// MergeMetadataJSON merges incoming metadata JSON into existing metadata.\n// Top-level keys from incoming overwrite keys in existing; keys only in\n// existing are preserved. Both inputs must be JSON objects (or empty/null).\nfunc MergeMetadataJSON(existing, incoming json.RawMessage) (json.RawMessage, error) {\n\tbase := make(map[string]json.RawMessage)\n\tif len(existing) > 0 {\n\t\ttrimmed := strings.TrimSpace(string(existing))\n\t\tif trimmed != \"\" && trimmed != \"null\" {\n\t\t\tif err := json.Unmarshal(existing, &base); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"existing metadata is not a JSON object: %w\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\toverlay := make(map[string]json.RawMessage)\n\tif err := json.Unmarshal(incoming, &overlay); err != nil {\n\t\treturn nil, fmt.Errorf(\"new metadata is not a JSON object: %w\", err)\n\t}\n\n\tfor k, v := range overlay {\n\t\tbase[k] = v\n\t}\n\n\tresult, err := json.Marshal(base)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal merged metadata: %w\", err)\n\t}\n\treturn json.RawMessage(result), nil\n}\n\n// ApplyMetadataEdits applies incremental set (key=value) and unset (key) edits\n// to existing metadata and returns the merged JSON. Set values are typed via\n// MetadataEditValue; keys are validated with ValidateMetadataKey.\nfunc ApplyMetadataEdits(existing json.RawMessage, setFlags, unsetFlags []string) (json.RawMessage, error) {\n\tdata := make(map[string]json.RawMessage)","sourceCodeStart":244,"sourceCodeEnd":280,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/metadata.go#L244-L280","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nincoming := json.RawMessage(`\"{\\\"k\\\":\\\"v\\\"}\"`) // double-encoded string\nmerged, err := MergeMetadataJSON(existing, incoming) // fails\n// after\nincoming := json.RawMessage(`{\"k\":\"v\"}`)\nmerged, err := MergeMetadataJSON(existing, incoming)","handlingStrategy":"validation","validationCode":"func validOverlay(raw json.RawMessage) bool {\n    var m map[string]json.RawMessage\n    return len(raw) > 0 && json.Unmarshal(raw, &m) == nil\n}\nif !validOverlay(incoming) { /* fix the payload before calling MergeMetadataJSON */ }","typeGuard":"func asOverlay(raw json.RawMessage) (map[string]json.RawMessage, bool) {\n    var m map[string]json.RawMessage\n    if err := json.Unmarshal(raw, &m); err != nil {\n        return nil, false\n    }\n    return m, true\n}","tryCatchPattern":"merged, err := MergeMetadataJSON(existing, incoming)\nif err != nil && strings.Contains(err.Error(), \"new metadata is not a JSON object\") {\n    // decode one layer if the payload was double-encoded\n    var s string\n    if json.Unmarshal(incoming, &s) == nil {\n        merged, err = MergeMetadataJSON(existing, json.RawMessage(s))\n    }\n}","preventionTips":["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."],"tags":["json","metadata","validation"],"backgroundTag":"metadata-not-json-object","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}