gastownhall/beads · error · issueops.ErrValidation
%w: expected value for metadata key %q: %v
Error message
%w: expected value for metadata key %q: %v
What it means
PlanCompareAndSetKey canonicalizes the expected and new pointer values via CanonicalMetadataPointer and wraps failures as issueops.ErrValidation with the key name and reason. Compare-and-set semantics depend on the expected value matching canonically, so both operands must be valid, canonical metadata pointers. The wrapped %v explains exactly what CanonicalMetadataPointer rejected (type or content).
Source
Thrown at internal/storage/metadata_cas.go:73
// It COPIES both raw values rather than aliasing the caller's, because the
// canonical form is written into the plan and the request belongs to the caller
// for the whole call.
func PlanCompareAndSetKey(in issueops.CompareAndSetKeyRequest) (CompareAndSetKeyPlan, error) {
if in.Actor == "" {
return CompareAndSetKeyPlan{}, fmt.Errorf(
"%w: compare-and-set requires an actor to attribute the swap to", issueops.ErrValidation)
}
if in.IssueID == "" {
return CompareAndSetKeyPlan{}, fmt.Errorf(
"%w: compare-and-set requires an issue id", issueops.ErrValidation)
}
if err := ValidateMetadataKey(in.Key); err != nil {
return CompareAndSetKeyPlan{}, fmt.Errorf("%w: %v", issueops.ErrValidation, err)
}
plan := CompareAndSetKeyPlan{Actor: in.Actor, IssueID: in.IssueID, Key: in.Key}
var err error
if plan.Expected, err = CanonicalMetadataPointer(in.Expected); err != nil {
return CompareAndSetKeyPlan{}, fmt.Errorf("%w: expected value for metadata key %q: %v",
issueops.ErrValidation, in.Key, err)
}
if plan.Value, err = CanonicalMetadataPointer(in.Value); err != nil {
return CompareAndSetKeyPlan{}, fmt.Errorf("%w: new value for metadata key %q: %v",
issueops.ErrValidation, in.Key, err)
}
return plan, nil
}
// CanonicalMetadataValue returns raw's canonical encoding: the encoding two
// JSON metadata values share exactly when issueops.MetadataCAS calls them
// equal.
//
// Insignificant whitespace goes and object keys are emitted in sorted order, so
// a value re-serialized by a different encoder still matches — the property
// that keeps a caller from losing a compare-and-set to its own formatting.
// Duplicate keys in one object collapse to the last, which is what every JSON
// reader in this tree already does with them.View on GitHub (pinned to 71377f2769)
Solutions
- Read the current stored value and canonicalize it (or use CanonicalMetadataPointer directly) before constructing Expected.
- Match the JSON type of the stored value exactly — quote strings: --metadata-json with \"expected\" not bare expected.
- Call CanonicalMetadataPointer on both Expected and Value in a pre-check and surface its error to the user.
- If uncertain of the current value, fetch it first rather than guessing; CAS exists to avoid blind overwrites.
Example fix
// before req.Value = json.RawMessage(`alice`) // bare word, invalid JSON plan, err := PlanCompareAndSetKey(req) // ErrValidation // after req.Value = json.RawMessage(`"alice"`) plan, err := PlanCompareAndSetKey(req)
Defensive patterns
Strategy: validation
Validate before calling
if _, err := CanonicalMetadataPointer(req.Expected); err != nil {
return fmt.Errorf("bad expected value: %v", err)
}
if _, err := CanonicalMetadataPointer(req.Value); err != nil {
return fmt.Errorf("bad new value: %v", err)
} // pre-check both operands before PlanCompareAndSetKey Type guard
func canonicalPointer(raw json.RawMessage) bool {
_, err := CanonicalMetadataPointer(raw)
return err == nil
} Try / catch
plan, err := PlanCompareAndSetKey(req)
if err != nil {
if errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), "value for metadata key") {
return fmt.Errorf("expected/new value rejected for key %q: fetch the stored value and match its JSON type", req.Key)
}
return err
} Prevention
- Read and reuse the canonical stored value as Expected instead of hand-writing it.
- Quote strings and emit real JSON types — never pass bare shell words as values.
- Run CanonicalMetadataPointer as a dry-run check when building requests dynamically.
When it happens
Trigger: Calling PlanCompareAndSetKey with in.Expected or in.Value that CanonicalMetadataPointer rejects — e.g. a non-JSON value, a value of the wrong type for the stored pointer, or a malformed raw fragment — e.g. passing "null" where a concrete value is required, or an object where a string pointer exists.
Common situations: Shell scripts passing unquoted values so scalars become something else; comparing against a value typed differently from what is stored (number vs string); building expected values by hand instead of reading the current stored value.
Related errors
- %w: compare-and-set requires an actor to attribute the swap
- %w: compare-and-set requires an issue id
- %w: %v
- %w: new value for metadata key %q: %v
- ExternalDoltConfig: must set Socket or (Host, Port)
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/80d0e5fc5f2bccf9.
Report an issue: GitHub.