{"record":{"id":"69672619559f6943","repo":"gastownhall/beads","slug":"w-new-value-for-metadata-key-q-v","errorCode":null,"errorMessage":"%w: new value for metadata key %q: %v","messagePattern":"%w: new value for metadata key %q: (.+?)","errorType":"validation","errorClass":"issueops.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/metadata_cas.go","lineNumber":77,"sourceCode":"\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.\n//\n// NUMBERS KEEP THEIR SOURCE LITERAL, so 1 and 1.0 canonicalize differently and\n// do not match. This function does not round-trip a number through float64, and\n// the reason is NOT that doing so would lose precision the store keeps: the","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/metadata_cas.go#L59-L95","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nvalue := json.RawMessage(`{\"status\" \"ok\"}`) // malformed, missing ':'\nreq := issueops.CompareAndSetKeyRequest{Value: &value}\n\n// after\nencoded, err := json.Marshal(map[string]string{\"status\": \"ok\"})\nif err != nil { return err }\nvalue := json.RawMessage(encoded)\nreq := issueops.CompareAndSetKeyRequest{Value: &value}","handlingStrategy":"validation","validationCode":"func validJSONValue(raw *json.RawMessage) error {\n    if raw == nil {\n        return nil // absent / remove — always acceptable\n    }\n    if !json.Valid(*raw) {\n        return fmt.Errorf(\"new metadata value is not valid JSON: %.64s\", *raw)\n    }\n    return nil\n}","typeGuard":"func isJSONRaw(b []byte) bool { return json.Valid(b) }","tryCatchPattern":null,"preventionTips":["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."],"tags":["validation","json","metadata","compare-and-set"],"backgroundTag":"invalid-json-metadata-value","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}