multica-ai/multica · warning

value cannot be null (use DELETE to remove a key)

Error message

value cannot be null (use DELETE to remove a key)

What it means

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.

Source

Thrown at server/internal/handler/issue_metadata.go:70

	return nil
}

// 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

View on GitHub (pinned to 2c0912b6ec)

Solutions

  1. To remove the key: call DELETE /issues/{id}/metadata/{key} instead of PUTting null.
  2. 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'.
  3. Strip null-valued entries before serializing the metadata payload.
  4. Add a client unit test that no metadata request body ever contains "value": null.

Example fix

// before
if (newValue === null) {
  await fetch(url, {method: 'PUT', body: JSON.stringify({value: null})});
}

// after
if (newValue === null) {
  await fetch(url, {method: 'DELETE'});
}
Defensive patterns

Strategy: type-guard

Validate before calling

function metadataAction(key, value) {
  if (value === null) {
    return { method: 'DELETE', url: metadataUrl(key) };
  }
  return { method: 'PUT', url: metadataUrl(key), body: JSON.stringify({ value }) };
}

Type guard

const isPrimitiveValue = (v) => v === null || typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';
// note: null is a valid JS value but maps to DELETE, not to a PUT body

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of multica-ai/multica@2c0912b6ec (2026-08-15). Data as JSON: /api/errors/5ea77c64bb27bba1. Report an issue: GitHub.