gastownhall/beads · error · issueops.ErrValidation
%w: new value for metadata key %q: %v
Error message
%w: new value for metadata key %q: %v
What it means
PlanCompareAndSetKey validates a compare-and-set request for a metadata key and canonicalizes both JSON values before any database work. This error is returned when the NEW value (the value to store, or nil to remove the key) fails canonicalization — typically because it is not well-formed JSON. It wraps issueops.ErrValidation, so callers can detect it with errors.Is as a pure request-validation refusal that consumed no storage work.
Source
Thrown at internal/storage/metadata_cas.go:77
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.
//
// NUMBERS KEEP THEIR SOURCE LITERAL, so 1 and 1.0 canonicalize differently and
// do not match. This function does not round-trip a number through float64, and
// the reason is NOT that doing so would lose precision the store keeps: theView on GitHub (pinned to 71377f2769)
Solutions
- Marshal the new value with json.Marshal (or json.Encoder) into json.RawMessage so the bytes are guaranteed well-formed JSON.
- Validate the bytes with json.Valid(value) before constructing CompareAndSetKeyRequest.
- Check the embedded %v detail — CanonicalMetadataValue's message quotes the offending (truncated) bytes — to find which caller supplied the malformed value.
- If the intent is to remove the key, pass a nil *json.RawMessage rather than empty or garbage bytes.
Example fix
// before
value := json.RawMessage(`{"status" "ok"}`) // malformed, missing ':'
req := issueops.CompareAndSetKeyRequest{Value: &value}
// after
encoded, err := json.Marshal(map[string]string{"status": "ok"})
if err != nil { return err }
value := json.RawMessage(encoded)
req := issueops.CompareAndSetKeyRequest{Value: &value} Defensive patterns
Strategy: validation
Validate before calling
func validJSONValue(raw *json.RawMessage) error {
if raw == nil {
return nil // absent / remove — always acceptable
}
if !json.Valid(*raw) {
return fmt.Errorf("new metadata value is not valid JSON: %.64s", *raw)
}
return nil
} Type guard
func isJSONRaw(b []byte) bool { return json.Valid(b) } Prevention
- Always produce metadata values with json.Marshal, never string concatenation.
- Call json.Valid on every RawMessage before building a CompareAndSetKeyRequest.
- Use nil *json.RawMessage (not empty bytes) to express 'absent' or 'remove'.
- Treat errors.Is(err, issueops.ErrValidation) as a caller bug, not a storage failure — no retry.
When it happens
Trigger: Calling PlanCompareAndSetKey (or any implementation of issueops.MetadataCAS.CompareAndSetKey) with CompareAndSetKeyRequest.Value set to a non-nil *json.RawMessage whose bytes are not valid JSON — e.g. []byte(`{foo`), truncated JSON, or unquoted text.
Common situations: Building the raw value by hand with fmt.Sprintf or string concatenation instead of json.Marshal; passing a Go struct's string form rather than its JSON encoding; copying a value out of a log or config file that is not JSON; a serializer upstream producing truncated output.
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/69672619559f6943.
Report an issue: GitHub.