{"record":{"id":"80d0e5fc5f2bccf9","repo":"gastownhall/beads","slug":"w-expected-value-for-metadata-key-q-v","errorCode":null,"errorMessage":"%w: expected value for metadata key %q: %v","messagePattern":"%w: expected value for metadata key %q: (.+?)","errorType":"validation","errorClass":"issueops.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/metadata_cas.go","lineNumber":73,"sourceCode":"// It COPIES both raw values rather than aliasing the caller's, because the\n// canonical form is written into the plan and the request belongs to the caller\n// for the whole call.\nfunc PlanCompareAndSetKey(in issueops.CompareAndSetKeyRequest) (CompareAndSetKeyPlan, error) {\n\tif in.Actor == \"\" {\n\t\treturn CompareAndSetKeyPlan{}, fmt.Errorf(\n\t\t\t\"%w: compare-and-set requires an actor to attribute the swap to\", issueops.ErrValidation)\n\t}\n\tif in.IssueID == \"\" {\n\t\treturn CompareAndSetKeyPlan{}, fmt.Errorf(\n\t\t\t\"%w: compare-and-set requires an issue id\", issueops.ErrValidation)\n\t}\n\tif err := ValidateMetadataKey(in.Key); err != nil {\n\t\treturn CompareAndSetKeyPlan{}, fmt.Errorf(\"%w: %v\", issueops.ErrValidation, err)\n\t}\n\tplan := CompareAndSetKeyPlan{Actor: in.Actor, IssueID: in.IssueID, Key: in.Key}\n\tvar err error\n\tif plan.Expected, err = CanonicalMetadataPointer(in.Expected); err != nil {\n\t\treturn CompareAndSetKeyPlan{}, fmt.Errorf(\"%w: expected value for metadata key %q: %v\",\n\t\t\tissueops.ErrValidation, in.Key, err)\n\t}\n\tif plan.Value, err = CanonicalMetadataPointer(in.Value); err != nil {\n\t\treturn CompareAndSetKeyPlan{}, fmt.Errorf(\"%w: new value for metadata key %q: %v\",\n\t\t\tissueops.ErrValidation, in.Key, err)\n\t}\n\treturn plan, nil\n}\n\n// CanonicalMetadataValue returns raw's canonical encoding: the encoding two\n// JSON metadata values share exactly when issueops.MetadataCAS calls them\n// equal.\n//\n// Insignificant whitespace goes and object keys are emitted in sorted order, so\n// a value re-serialized by a different encoder still matches — the property\n// that keeps a caller from losing a compare-and-set to its own formatting.\n// Duplicate keys in one object collapse to the last, which is what every JSON\n// reader in this tree already does with them.","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/metadata_cas.go#L55-L91","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nreq.Value = json.RawMessage(`alice`) // bare word, invalid JSON\nplan, err := PlanCompareAndSetKey(req) // ErrValidation\n// after\nreq.Value = json.RawMessage(`\"alice\"`)\nplan, err := PlanCompareAndSetKey(req)","handlingStrategy":"validation","validationCode":"if _, err := CanonicalMetadataPointer(req.Expected); err != nil {\n    return fmt.Errorf(\"bad expected value: %v\", err)\n}\nif _, err := CanonicalMetadataPointer(req.Value); err != nil {\n    return fmt.Errorf(\"bad new value: %v\", err)\n} // pre-check both operands before PlanCompareAndSetKey","typeGuard":"func canonicalPointer(raw json.RawMessage) bool {\n    _, err := CanonicalMetadataPointer(raw)\n    return err == nil\n}","tryCatchPattern":"plan, err := PlanCompareAndSetKey(req)\nif err != nil {\n    if errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), \"value for metadata key\") {\n        return fmt.Errorf(\"expected/new value rejected for key %q: fetch the stored value and match its JSON type\", req.Key)\n    }\n    return err\n}","preventionTips":["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."],"tags":["validation","metadata","compare-and-set"],"backgroundTag":"invalid-metadata-value","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}