{"record":{"id":"3375b6f7b02608af","repo":"multica-ai/multica","slug":"value-must-be-a-primitive-string-number-or-bool","errorCode":null,"errorMessage":"value must be a primitive: string, number, or bool","messagePattern":"value must be a primitive: string, number, or bool","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"server/internal/handler/issue_metadata.go","lineNumber":72,"sourceCode":"\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\n// a nil []byte, which the SQL layer interprets as \"skip the @> check\".\n//","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/handler/issue_metadata.go#L54-L90","documentation":"validateIssueMetadataValue rejects values that are not primitive JSON scalars: arrays and objects fail this branch (null fails earlier with its own message; invalid JSON fails the unmarshal step). The V1 metadata surface is deliberately flat — a JSONB object of scalars — so nested structures are out of contract. Clients that need structure must flatten it into multiple keys.","triggerScenarios":"PUT with {\"value\":[\"a\",\"b\"]}, {\"value\":{\"nested\":true}}, or {\"value\":{\"a\":[1]}}. Any JSON array or object literal as the value triggers it.","commonSituations":"Client stuffing a whole config object or tag list under one key; schema drift where a client upgrades to structured metadata before the server does; form state serialized wholesale as the value.","solutions":["Flatten structures into multiple keys (e.g. tags.0, tags.1 or tags='a,b') matching the key regex.","If structure is required, keep it out of this endpoint — model it as a first-class resource instead.","Validate client-side: typeof value in {'string','number','boolean'} before sending.","For lists, consider a comma/newline-delimited string if the reader can parse it."],"exampleFix":"// before\nawait putMetadata(id, 'tags', ['a', 'b']); // sends {\"value\":[\"a\",\"b\"]}\n\n// after\nawait putMetadata(id, 'tags', 'a,b'); // primitive string\n// or multiple keys: 'tags.0'='a', 'tags.1'='b'","handlingStrategy":"type-guard","validationCode":"function assertPrimitiveValue(value) {\n  const t = typeof value;\n  if (t !== 'string' && t !== 'number' && t !== 'boolean') {\n    throw new TypeError(`metadata value must be string|number|boolean, got ${t}`);\n  }\n}","typeGuard":"const isMetadataScalar = (v) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';","tryCatchPattern":null,"preventionTips":["Flatten objects/arrays into multiple keys or delimited strings at the client edge.","Type the metadata bag as Record<string, string | number | boolean> in TS so structures fail to compile.","If structured metadata is a real need, propose extending the API rather than smuggling JSON strings."],"tags":["validation","metadata","type-mismatch","http-400"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}