chroma-core/chroma · error · ValueError

Expected metadata value to be a str, int, float, bool, Spars

Error message

Expected metadata value to be a str, int, float, bool, SparseVector, list, or None, got {value} which is a {type(value).__name__}

What it means

Scalar metadata values in the insert path may be str, int, float, bool, None, a SparseVector, or a homogeneous list of str/int/float/bool (list rules enforced by _validate_metadata_list_value). Anything else - nested dicts, tuples, sets, datetime/date, Decimal, bytes, numpy scalars - raises this ValueError naming the offending value and type. This is Chroma's flat-metadata constraint: where-filters only operate on scalar fields.

Source

Thrown at chromadb/api/types.py:1099

    for key, value in metadata.items():
        if key == META_KEY_CHROMA_DOCUMENT:
            raise ValueError(
                f"Expected metadata to not contain the reserved key {META_KEY_CHROMA_DOCUMENT}"
            )
        if not isinstance(key, str):
            raise TypeError(
                f"Expected metadata key to be a str, got {key} which is a {type(key).__name__}"
            )
        # Check if value is a SparseVector (validation happens in __post_init__)
        if isinstance(value, SparseVector):
            pass  # Already validated in SparseVector.__post_init__
        elif isinstance(value, list):
            _validate_metadata_list_value(key, value)
        # isinstance(True, int) evaluates to True, so we need to check for bools separately
        elif not isinstance(value, bool) and not isinstance(
            value, (str, int, float, type(None))
        ):
            raise ValueError(
                f"Expected metadata value to be a str, int, float, bool, SparseVector, list, or None, got {value} which is a {type(value).__name__}"
            )
    return metadata


def validate_update_metadata(metadata: UpdateMetadata) -> UpdateMetadata:
    """Validates metadata to ensure it is a dictionary of strings to strings, ints, floats, bools, SparseVectors, or lists thereof"""
    if not isinstance(metadata, dict) and metadata is not None:
        raise ValueError(
            f"Expected metadata to be a dict or None, got {type(metadata)}"
        )
    if metadata is None:
        return metadata
    if len(metadata) == 0:
        raise ValueError(f"Expected metadata to be a non-empty dict, got {metadata}")
    for key, value in metadata.items():
        if not isinstance(key, str):
            raise ValueError(f"Expected metadata key to be a str, got {key}")

View on GitHub (pinned to aecdd12c8a)

Solutions

  1. Flatten or serialize: timestamps to .isoformat() strings or epoch int/float; Decimal to float(); nested structures to json.dumps(...) stored as str
  2. Convert numpy scalars with .item() before building metadata
  3. Keep metadata strictly scalar and move rich content into documents (free text)
  4. Write one normalize_metadata() helper and route every write path through it

Example fix

# before
meta = {'created_at': row['ts'], 'price': row['price']}  # datetime, Decimal

# after
meta = {'created_at': row['ts'].isoformat(), 'price': float(row['price'])}
Defensive patterns

Strategy: type-guard

Validate before calling

import json
from datetime import datetime, date

def normalize_meta_value(v):
    if isinstance(v, (datetime, date)):
        return v.isoformat()
    if hasattr(v, 'item'):  # numpy scalar
        return v.item()
    if isinstance(v, tuple):
        return list(v)
    if isinstance(v, (dict, set, bytes)):
        return json.dumps(v, default=str) if not isinstance(v, bytes) else v.decode('utf-8', 'replace')
    if v.__class__.__name__ == 'Decimal':
        return float(v)
    return v

def normalize_metadata(meta):
    return {k: normalize_meta_value(v) for k, v in meta.items()}

Type guard

def is_flat_metadata_value(v) -> bool:
    if v is None or isinstance(v, (str, int, float, bool)):
        return True
    if isinstance(v, list) and v:
        ts = {bool if isinstance(x, bool) else type(x) for x in v}
        return len(ts) == 1 and next(iter(ts)) in (str, int, float, bool)
    return False

Try / catch

try:
    collection.add(ids=ids, metadatas=metas)
except ValueError as e:
    if 'Expected metadata value to be a str, int, float, bool' in str(e):
        metas = [normalize_metadata(m) for m in metas]
        collection.add(ids=ids, metadatas=metas)
    else:
        raise

Prevention

When it happens

Trigger: metadatas=[{'created_at': datetime.now()}]; {'price': Decimal('1.99')}; {'nested': {'a': 1}}; {'pair': (1, 2)} (tuple is not a list); {'conf': np.float32(0.9)}; bytes blobs from binary columns.

Common situations: Datetime columns from ORMs or pandas (df datetime64 values via .item() are still datetime objects); Decimal from money columns; nested JSON from webhooks; numpy scalars leaking from vectorized code; tuple-valued fields assumed to count as lists.

Related errors


AI-assisted analysis of chroma-core/chroma@aecdd12c8a (2026-08-16). Data as JSON: /api/errors/685233eab5c94e3d. Report an issue: GitHub.