{"record":{"id":"5ea77c64bb27bba1","repo":"multica-ai/multica","slug":"value-cannot-be-null-use-delete-to-remove-a-key","errorCode":null,"errorMessage":"value cannot be null (use DELETE to remove a key)","messagePattern":"value cannot be null \\(use DELETE to remove a key\\)","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"server/internal/handler/issue_metadata.go","lineNumber":70,"sourceCode":"\treturn nil\n}\n\n// validateIssueMetadataValue rejects anything other than a primitive JSON\n// scalar. Null, arrays, and objects are not allowed — the V1 surface is\n// flat KV. Removing a key uses DELETE, not a null value.\nfunc validateIssueMetadataValue(raw json.RawMessage) error {\n\tif len(raw) == 0 {\n\t\treturn errors.New(\"value is required\")\n\t}\n\tvar v any\n\tif err := json.Unmarshal(raw, &v); err != nil {\n\t\treturn fmt.Errorf(\"value must be valid JSON: %w\", err)\n\t}\n\tswitch v.(type) {\n\tcase string, bool, float64:\n\t\treturn nil\n\tcase nil:\n\t\treturn errors.New(\"value cannot be null (use DELETE to remove a key)\")\n\tdefault:\n\t\treturn errors.New(\"value must be a primitive: string, number, or bool\")\n\t}\n}\n\n// parseIssueMetadata decodes the JSONB bytes from db.Issue.Metadata into a\n// Go map suitable for response serialization. Empty or unparseable blobs\n// degrade to an empty map — the DB CHECK guarantees object shape, so this\n// path is only hit on rows somehow predating the migration. Shared with the\n// service-layer broadcast rendering (service.IssueToMap) so both ways of\n// describing an issue agree on what an unset bag looks like on the wire.\nfunc parseIssueMetadata(raw []byte) map[string]any {\n\treturn util.JSONObjectOrEmpty(raw)\n}\n\n// parseMetadataFilterParam reads the `metadata` query parameter (a JSON\n// object) and returns it as the JSONB filter blob passed to ListIssues /\n// CountIssues / ListOpenIssues. Empty input means \"no filter\" and returns","sourceCodeStart":52,"sourceCodeEnd":88,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/handler/issue_metadata.go#L52-L88","documentation":"validateIssueMetadataValue explicitly rejects JSON null as a metadata value. Flat KV metadata V1 stores only primitive scalars (string, number, bool); removal is expressed through the DELETE verb, not through nulling the key, so a null PUT would create an ambiguous 'key exists with no value' state. The error message points the caller at the correct verb.","triggerScenarios":"PUT /issues/{id}/metadata/{key} with body {\"value\":null}. Common when client code sets a field to null to 'clear' it and serializes the whole object; or when a patch-style UI sends null for empty form fields.","commonSituations":"JSON serializers emitting null for unset fields (Golang pointers, TS nullable fields); CRUD clients reusing update semantics where null means clear; spreadsheet-style editors distinguishing empty ('') from missing (null) and sending null for missing.","solutions":["To remove the key: call DELETE /issues/{id}/metadata/{key} instead of PUTting null.","Map client-side null/undefined to 'do not send this key at all' (skip the request) or to empty string '' if you mean 'present but empty'.","Strip null-valued entries before serializing the metadata payload.","Add a client unit test that no metadata request body ever contains \"value\": null."],"exampleFix":"// before\nif (newValue === null) {\n  await fetch(url, {method: 'PUT', body: JSON.stringify({value: null})});\n}\n\n// after\nif (newValue === null) {\n  await fetch(url, {method: 'DELETE'});\n}","handlingStrategy":"type-guard","validationCode":"function metadataAction(key, value) {\n  if (value === null) {\n    return { method: 'DELETE', url: metadataUrl(key) };\n  }\n  return { method: 'PUT', url: metadataUrl(key), body: JSON.stringify({ value }) };\n}","typeGuard":"const isPrimitiveValue = (v) => v === null || typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';\n// note: null is a valid JS value but maps to DELETE, not to a PUT body","tryCatchPattern":null,"preventionTips":["Translate null/undefined to DELETE at the client boundary instead of serializing it.","Use a strict serializer that throws on null values in metadata payloads rather than emitting them.","Document the DELETE-to-remove convention in the client SDK so contributors don't reinvent null-clearing."],"tags":["validation","metadata","null-handling","http-400"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}