chroma-core/chroma · error · ValueError

Expected metadata to be a dict or None, got {type(metadata)}

Error message

Expected metadata to be a dict or None, got {type(metadata)}

What it means

The update-path twin of the insert validator: validate_update_metadata (chromadb/api/types.py:1105), invoked per record from SegmentAPI._update (chromadb/segment.py:407) when you call collection.update(ids=..., metadatas=[...]). Each metadatas element must be a dict or None; anything else - an unparsed JSON string, a tuple, a scalar - raises. Unlike the insert variant, the message prints the full type repr (e.g. "<class 'str'>").

Source

Thrown at chromadb/api/types.py:1108

        # 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}")
        # 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))
        ):

View on GitHub (pinned to aecdd12c8a)

Solutions

  1. Ensure metadatas is a list of dicts (or None per record): json.loads any serialized values first
  2. Match the shape used by add(): one dict per id, aligned with the ids list
  3. Validate the shape once in your update wrapper before calling Chroma

Example fix

# before
collection.update(ids=ids, metadatas=cached_meta)  # cached_meta is a JSON string

# after
collection.update(ids=ids, metadatas=[json.loads(cached_meta)])
Defensive patterns

Strategy: type-guard

Validate before calling

import json

def ensure_update_metadatas(metadatas):
    if metadatas is None:
        return None
    if not isinstance(metadatas, list):
        metadatas = [metadatas]
    return [json.loads(m) if isinstance(m, str) else m for m in metadatas]

collection.update(ids=ids, metadatas=ensure_update_metadatas(metadatas))

Type guard

def is_valid_update_metadata(m) -> bool:
    return m is None or isinstance(m, dict)

Try / catch

try:
    collection.update(ids=ids, metadatas=metas)
except ValueError as e:
    if 'Expected metadata to be a dict or None' in str(e):
        metas = [json.loads(m) if isinstance(m, str) else m for m in metas]
        collection.update(ids=ids, metadatas=metas)
    else:
        raise

Prevention

When it happens

Trigger: collection.update(ids=ids, metadatas=meta_str) where meta_str is a serialized JSON string (wrapped into one non-dict element); metadatas=[None, ('a', 1)]; metadatas=[123]; caches or queues delivering metadata as strings.

Common situations: Updating rows whose metadata came from a queue or cache in serialized form; passing a single string instead of a list; heterogeneous sources feeding the same update call.

Related errors


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