gastownhall/beads · error · storage.ErrValidation
%w: metadata merge must be a JSON object
Error message
%w: metadata merge must be a JSON object
What it means
ApplyMetadataPatch wraps storage.ErrValidation with this message when patch.Merge.Set is true and the merge overlay is the literal JSON null. A JSON null would unmarshal into a nil map and silently do nothing, so the library explicitly rejects it to prevent a no-op being mistaken for a successful merge.
Source
Thrown at internal/storage/issueops/aggregate.go:223
return nil, false, fmt.Errorf("%w: %w", storage.ErrValidation, err)
}
}
var next json.RawMessage
if patch.Replace.Set {
next = append(json.RawMessage(nil), patch.Replace.Value...)
if len(next) == 0 {
next = json.RawMessage(`{}`)
}
if !json.Valid(next) {
return nil, false, fmt.Errorf("%w: metadata replacement is not valid JSON", storage.ErrValidation)
}
} 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)View on GitHub (pinned to 71377f2769)
Solutions
- Check the merge value for "null" (after trimming) in the caller and treat it as absent: leave patch.Merge.Set false.
- Marshal only non-nil maps/structs for the merge overlay.
- Fix the upstream data source so a real JSON object is supplied when a merge is intended.
Example fix
// before
patch.Merge.Set = true
patch.Merge.Value = json.RawMessage(overlayJSON) // overlayJSON may be "null"
// after
trimmed := strings.TrimSpace(string(overlayJSON))
if trimmed != "" && trimmed != "null" {
patch.Merge.Set = true
patch.Merge.Value = json.RawMessage(overlayJSON)
} Defensive patterns
Strategy: validation
Validate before calling
func validMergeOverlay(v json.RawMessage) bool {
t := strings.TrimSpace(string(v))
return t != "" && t != "null" && strings.HasPrefix(t, "{")
} Try / catch
if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), "must be a JSON object") { /* set Merge.Set=false or supply a real object */ } Prevention
- Never set patch.Merge.Set=true for null overlays; treat null as absent.
- Check for the literal "null" after trimming before enabling Merge.
- Marshal only non-nil maps/structs as merge overlays.
- Reject null-typed optional fields at the config-parsing layer.
When it happens
Trigger: Calling ApplyMetadataPatch with patch.Merge.Set=true and patch.Merge.Value trimming to "null" — e.g. a variable holding JSON that was null, or an unset flag serialized as null.
Common situations: Passing optional merge input from JSON config where the field is null; Go nil pointer/slice marshaled to null and fed straight into the merge patch; CLI flags defaulting to the string "null".
Related errors
- ExternalDoltConfig: must set Socket or (Host, Port)
- %w: metadata replacement is not valid JSON
- %w: metadata merge: %v
- %w: metadata edits require an object: %v
- %w: metadata value for key %q is not valid JSON
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/b1a933ab4af20fd4.
Report an issue: GitHub.