gastownhall/beads · error
metadata replacement cannot combine with incremental edits
Error message
metadata replacement cannot combine with incremental edits
What it means
validateMetadataPatch enforces that a metadata Replace (full replacement) is mutually exclusive with incremental edits (Merge, Set, Unset). Mixing them is ambiguous, so the patch is rejected as a plain error inside the validation path.
Source
Thrown at internal/storage/uow/issue_operations.go:547
if err := storageissueops.ValidateUpdateRequest(request); err != nil {
return validationError(err)
}
if err := validateMetadataPatch(request.Patch.Metadata); err != nil {
return validationError(err)
}
return nil
}
func validateCloseRequest(request publicops.CloseRequest) error {
if request.Actor == "" || request.IssueID == "" {
return validationError(fmt.Errorf("close: actor and issue ID must not be empty"))
}
return nil
}
func validateMetadataPatch(metadata publicops.MetadataPatch) error {
if metadata.Replace.Set && (metadata.Merge.Set || len(metadata.Set) > 0 || len(metadata.Unset) > 0) {
return fmt.Errorf("metadata replacement cannot combine with incremental edits")
}
if metadata.Replace.Set && len(metadata.Replace.Value) > 0 && !json.Valid(metadata.Replace.Value) {
return fmt.Errorf("metadata replacement is not valid JSON")
}
if metadata.Merge.Set {
var object map[string]json.RawMessage
if len(metadata.Merge.Value) == 0 || json.Unmarshal(metadata.Merge.Value, &object) != nil || object == nil {
return fmt.Errorf("metadata merge must be a JSON object")
}
}
keys := make([]string, 0, len(metadata.Set))
for key := range metadata.Set {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
if err := storage.ValidateMetadataKey(key); err != nil {
return errView on GitHub (pinned to 71377f2769)
Solutions
- Choose one strategy: either Replace alone or incremental edits (Merge/Set/Unset) alone
- If you need replacement plus tweaks, compute the final object and use Replace only
- If you need targeted changes, drop Replace.Set and use Set/Unset/Merge
- Add a builder guard so patch constructors cannot set both
Example fix
// before patch.Metadata.Replace.Set = true patch.Metadata.Replace.Value = newJSON patch.Metadata.Set["priority"] = json.RawMessage(`"high"`) // after patch.Metadata.Replace.Set = true patch.Metadata.Replace.Value = fullMetadataJSON // includes priority // (no Set/Unset/Merge alongside Replace)
Defensive patterns
Strategy: validation
Validate before calling
func validMetadataPatch(p publicops.MetadataPatch) error {
if p.Replace.Set && (p.Merge.Set || len(p.Set) > 0 || len(p.Unset) > 0) {
return fmt.Errorf("replace is mutually exclusive with merge/set/unset")
}
return nil
} Try / catch
if err := uc.Update(ctx, req); err != nil {
if errors.Is(err, publicops.ErrValidation) && strings.Contains(err.Error(), "cannot combine") {
// rebuild patch with a single metadata strategy
}
return err
} Prevention
- Pick one metadata-editing strategy per update (replace OR incremental)
- Compose patch fields in a single builder function, not from multiple flag sources
- Add a unit test asserting Replace+Set is rejected in your patch builder
- Document the exclusivity rule where patch types are constructed
When it happens
Trigger: Calling Update with request.Patch.Metadata where Replace.Set is true AND (Merge.Set is true, or len(Set) > 0, or len(Unset) > 0).
Common situations: Composing an update from multiple config sources where one sets Replace and another adds Set/Unset keys; merging user-supplied flags like --metadata-replace together with --set/--unset; copy-paste of patch-building code.
Related errors
- metadata replacement is not valid JSON
- metadata merge must be a JSON object
- metadata value for key %q is not valid JSON
- no store is open for this workspace
- not found
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/7d2cdf618d41a1ed.
Report an issue: GitHub.