{"record":{"id":"d43ef10930c57a9b","repo":"chroma-core/chroma","slug":"expected-where-to-have-exactly-one-operator-got","errorCode":null,"errorMessage":"Expected where to have exactly one operator, got {where}","messagePattern":"Expected where to have exactly one operator, got (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"chromadb/api/types.py","lineNumber":1194,"sourceCode":"\ndef validate_metadatas(metadatas: Metadatas) -> Metadatas:\n    \"\"\"Validates metadatas to ensure it is a list of dictionaries of strings to strings, ints, floats or bools\"\"\"\n    if not isinstance(metadatas, list):\n        raise ValueError(f\"Expected metadatas to be a list, got {metadatas}\")\n    for metadata in metadatas:\n        validate_metadata(metadata)\n    return metadatas\n\n\ndef validate_where(where: Where) -> None:\n    \"\"\"\n    Validates where to ensure it is a dictionary of strings to strings, ints, floats or operator expressions,\n    or in the case of $and and $or, a list of where expressions\n    \"\"\"\n    if not isinstance(where, dict):\n        raise ValueError(f\"Expected where to be a dict, got {where}\")\n    if len(where) != 1:\n        raise ValueError(f\"Expected where to have exactly one operator, got {where}\")\n    for key, value in where.items():\n        if not isinstance(key, str):\n            raise ValueError(f\"Expected where key to be a str, got {key}\")\n        # $contains and $not_contains are only valid as operators within a\n        # field expression (e.g. {\"field\": {\"$contains\": val}}), not as\n        # top-level where keys.\n        if key in (\"$contains\", \"$not_contains\"):\n            raise ValueError(\n                f\"Expected where key to be a metadata field name or a logical \"\n                f\"operator ($and, $or), got {key}\"\n            )\n        if (\n            key != \"$and\"\n            and key != \"$or\"\n            and key != \"$in\"\n            and key != \"$nin\"\n            and not isinstance(value, (str, int, float, dict))\n        ):","sourceCodeStart":1176,"sourceCodeEnd":1212,"githubUrl":"https://github.com/chroma-core/chroma/blob/aecdd12c8a891610db8653630b066b32ceb678b5/chromadb/api/types.py#L1176-L1212","documentation":"A ChromaDB `where` clause must contain exactly one operator at each level: either one field expression (\"field\": value/operatorexpr) or one logical operator ($and/$or). This ValueError fires when the dict has zero or two-plus keys, e.g. {\"a\": 1, \"b\": 2} or {}. The single-key grammar is what allows the validator to distinguish a field filter from a logical combinator, and it is enforced recursively.","triggerScenarios":"Passing where={\"source\": \"wiki\", \"year\": 2024} (two fields, no $and), where={} (empty dict, often from a filter builder that added nothing), or nesting a multi-key dict inside $and, e.g. {\"$and\": [{\"a\": 1, \"b\": 2}]}.","commonSituations":"Developers assuming Mongo-style multi-key filtering where {\"a\":1,\"b\":2} means implicit AND; auto-generated filters where each optional predicate is dict-merged (d1 | d2) instead of appended to an $and list; empty filters produced when no user filters are selected but the code still passes `where`.","solutions":["Rewrite multi-field filters with $and: {\"$and\": [{\"source\": \"wiki\"}, {\"year\": 2024}]} (list needs >= 2 entries).","Build filters incrementally by appending single-key dicts to a list and wrapping in $and only if len > 1; pass where=None when the list is empty.","Check for typos that accidentally merge keys, e.g. {**base, **extra} on where dicts."],"exampleFix":"# before\nwhere = {\"source\": \"wiki\", \"year\": 2024}\n# after\nwhere = {\"$and\": [{\"source\": \"wiki\"}, {\"year\": 2024}]}","handlingStrategy":"validation","validationCode":"def build_where(conditions: list[dict]) -> dict | None:\n    \"\"\"Combine single-key condition dicts into a valid Chroma where.\"\"\"\n    if not conditions:\n        return None\n    if len(conditions) == 1:\n        return conditions[0]\n    return {\"$and\": conditions}\n\n# usage: every element must itself have exactly one key\nassert all(len(c) == 1 for c in conditions)","typeGuard":"def is_single_key(where: dict) -> bool:\n    return len(where) == 1","tryCatchPattern":"try:\n    result = collection.get(where=where)\nexcept ValueError as e:\n    if \"exactly one operator\" in str(e):\n        # flatten multi-key dict into $and and retry once\n        conds = [{k: v} for k, v in where.items()]\n        where = conds[0] if len(conds) == 1 else {\"$and\": conds}\n        result = collection.get(where=where)\n    else:\n        raise","preventionTips":["Never dict-merge conditions ({**a, **b}); append to a list instead.","Centralize filter construction in a builder that enforces the one-key rule.","Wrap condition lists in $and only when len >= 2; use the condition directly when len == 1.","Property-test your filter builder: every output must pass validate_where."],"tags":["chromadb","validation","where-filter","query"],"backgroundTag":"invalid-query-filter","analyzedSha":"aecdd12c8a891610db8653630b066b32ceb678b5","analyzedAt":"2026-08-16T21:53:27.228Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}