multica-ai/multica · warning
value must be a primitive: string, number, or bool
Error message
value must be a primitive: string, number, or bool
What it means
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.
Source
Thrown at server/internal/handler/issue_metadata.go:72
// validateIssueMetadataValue rejects anything other than a primitive JSON
// scalar. Null, arrays, and objects are not allowed — the V1 surface is
// flat KV. Removing a key uses DELETE, not a null value.
func validateIssueMetadataValue(raw json.RawMessage) error {
if len(raw) == 0 {
return errors.New("value is required")
}
var v any
if err := json.Unmarshal(raw, &v); err != nil {
return fmt.Errorf("value must be valid JSON: %w", err)
}
switch v.(type) {
case string, bool, float64:
return nil
case nil:
return errors.New("value cannot be null (use DELETE to remove a key)")
default:
return errors.New("value must be a primitive: string, number, or bool")
}
}
// parseIssueMetadata decodes the JSONB bytes from db.Issue.Metadata into a
// Go map suitable for response serialization. Empty or unparseable blobs
// degrade to an empty map — the DB CHECK guarantees object shape, so this
// path is only hit on rows somehow predating the migration. Shared with the
// service-layer broadcast rendering (service.IssueToMap) so both ways of
// describing an issue agree on what an unset bag looks like on the wire.
func parseIssueMetadata(raw []byte) map[string]any {
return util.JSONObjectOrEmpty(raw)
}
// parseMetadataFilterParam reads the `metadata` query parameter (a JSON
// object) and returns it as the JSONB filter blob passed to ListIssues /
// CountIssues / ListOpenIssues. Empty input means "no filter" and returns
// a nil []byte, which the SQL layer interprets as "skip the @> check".
//View on GitHub (pinned to 2c0912b6ec)
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.
Example fix
// before
await putMetadata(id, 'tags', ['a', 'b']); // sends {"value":["a","b"]}
// after
await putMetadata(id, 'tags', 'a,b'); // primitive string
// or multiple keys: 'tags.0'='a', 'tags.1'='b' Defensive patterns
Strategy: type-guard
Validate before calling
function assertPrimitiveValue(value) {
const t = typeof value;
if (t !== 'string' && t !== 'number' && t !== 'boolean') {
throw new TypeError(`metadata value must be string|number|boolean, got ${t}`);
}
} Type guard
const isMetadataScalar = (v) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';
Prevention
- 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.
When it happens
Trigger: PUT with {"value":["a","b"]}, {"value":{"nested":true}}, or {"value":{"a":[1]}}. Any JSON array or object literal as the value triggers it.
Common situations: 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.
Related errors
- key is required
- key must match ^[a-zA-Z_][a-zA-Z0-9_.-]{0,63}$
- value is required
- value cannot be null (use DELETE to remove a key)
- invalid limit
AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15).
Data as JSON: /api/errors/3375b6f7b02608af.
Report an issue: GitHub.