chroma-core/chroma · error · ValueError

Expected metadata to be a non-empty dict, got {metadata}

Error message

Expected metadata to be a non-empty dict, got {metadata}

What it means

validate_update_metadata rejects {} as a per-record metadata on update (chromadb/api/types.py:1114, reached from collection.update via chromadb/segment.py:407). An empty update dict is treated as an invalid input rather than 'change nothing'. The correct way to express 'nothing to change' for a record is to omit it from the ids/metadatas lists, or use None as the element.

Source

Thrown at chromadb/api/types.py:1114

        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))
        ):
            raise ValueError(
                f"Expected metadata value to be a str, int, float, bool, SparseVector, list, or None, got {value}"
            )
    return metadata

View on GitHub (pinned to aecdd12c8a)

Solutions

  1. Skip unchanged rows: only include ids whose metadata diff is non-empty
  2. Send None instead of {}: metadatas=[m if m else None for m in metadatas]
  3. Split the batch so only genuinely changed records are updated

Example fix

# before
collection.update(ids=all_ids, metadatas=[diff(r) for r in rows])  # some diffs are {}

# after
changed = [(i, d) for i, d in zip(all_ids, diffs) if d]
collection.update(ids=[i for i, _ in changed], metadatas=[d for _, d in changed])
Defensive patterns

Strategy: validation

Validate before calling

pairs = [(i, m) for i, m in zip(ids, metadatas) if m]
if pairs:
    collection.update(ids=[i for i, _ in pairs], metadatas=[m for _, m in pairs])

Type guard

def has_no_empty_update_dicts(metadatas) -> bool:
    return all(m is None or len(m) > 0 for m in metadatas)

Try / catch

try:
    collection.update(ids=ids, metadatas=metas)
except ValueError as e:
    if 'non-empty dict' in str(e):
        collection.update(ids=ids, metadatas=[m if m else None for m in metas])
    else:
        raise

Prevention

When it happens

Trigger: collection.update(ids=['a'], metadatas=[{}]); batch updates where some rows have no changed fields; diff-based update builders that emit {} for unchanged rows.

Common situations: Change-data-capture pipelines computing per-row diffs and producing empty dicts for unchanged rows; UI save handlers that send the whole object including empty metadata; generic update loops over all rows regardless of change.

Related errors


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