{"record":{"id":"bea4173622e9625e","repo":"chroma-core/chroma","slug":"expected-where-document-operand-value-for-operator","errorCode":null,"errorMessage":"Expected where document operand value for operator {operator} to be a str, got {operand}","messagePattern":"Expected where document operand value for operator (.+?) to be a str, got (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"chromadb/api/types.py","lineNumber":1326,"sourceCode":"            \"$or\",\n        ]:\n            raise ValueError(\n                f\"Expected where document operator to be one of $contains, $not_contains, $regex, $not_regex, $and, $or, got {operator}\"\n            )\n        if operator == \"$and\" or operator == \"$or\":\n            if not isinstance(operand, list):\n                raise ValueError(\n                    f\"Expected document value for $and or $or to be a list of where document expressions, got {operand}\"\n                )\n            if len(operand) <= 1:\n                raise ValueError(\n                    f\"Expected document value for $and or $or to be a list with at least two where document expressions, got {operand}\"\n                )\n            for where_document_expression in operand:\n                validate_where_document(where_document_expression)\n        # Value is $contains/$not_contains/$regex/$not_regex operator\n        elif not isinstance(operand, str):\n            raise ValueError(\n                f\"Expected where document operand value for operator {operator} to be a str, got {operand}\"\n            )\n        elif len(operand) == 0:\n            raise ValueError(\n                f\"Expected where document operand value for operator {operator} to be a non-empty str\"\n            )\n\n\ndef validate_include(include: Include, dissalowed: Optional[Include] = None) -> None:\n    \"\"\"Validates include to ensure it is a list of strings. Since get does not allow distances, allow_distances is used\n    to control if distances is allowed\"\"\"\n\n    if not isinstance(include, list):\n        raise ValueError(f\"Expected include to be a list, got {include}\")\n    for item in include:\n        if not isinstance(item, str):\n            raise ValueError(f\"Expected include item to be a str, got {item}\")\n","sourceCodeStart":1308,"sourceCodeEnd":1344,"githubUrl":"https://github.com/chroma-core/chroma/blob/aecdd12c8a891610db8653630b066b32ceb678b5/chromadb/api/types.py#L1308-L1344","documentation":"In validate_where_document, when the operator is one of $contains, $not_contains, $regex, $not_regex (i.e. not a logical operator), the operand must be a Python str. Passing any non-string (int, list, dict, None) raises this ValueError before the query executes.","triggerScenarios":"collection.query(query_embeddings=..., where_document={\"$contains\": 123}) or {\"$not_contains\": [\"a\", \"b\"]} or {\"$regex\": None}. Also happens when a variable used as the operand is unexpectedly None or a list, e.g. an unrolled parameter from an API request.","commonSituations":"Passing a list expecting OR-semantics (Chroma needs $or with multiple $contains clauses); a template/config value that arrived as JSON number or null; forgetting that $regex takes the pattern string itself, not a compiled pattern or a (pattern, flags) tuple.","solutions":["Pass a plain string operand: where_document={\"$contains\": \"hello\"}","For multiple alternatives use {\"$or\": [{\"$contains\": \"a\"}, {\"$contains\": \"b\"}]}","Cast/validate the value before the call: operand = str(operand) if operand is not None else skip the filter"],"exampleFix":"# before\nwhere_document = {\"$contains\": [\"hello\", \"world\"]}\n\n# after\nwhere_document = {\"$or\": [\n    {\"$contains\": \"hello\"},\n    {\"$contains\": \"world\"},\n]}","handlingStrategy":"type-guard","validationCode":"def normalize_where_document(wd: dict) -> dict:\n    out = {}\n    for op, operand in wd.items():\n        if op in (\"$contains\", \"$not_contains\", \"$regex\", \"$not_regex\"):\n            if not isinstance(operand, str):\n                raise TypeError(f\"{op} needs a str operand, got {type(operand).__name__}\")\n            out[op] = operand\n        else:\n            out[op] = operand\n    return out\n\nres = collection.get(where_document=normalize_where_document(wd))","typeGuard":"STRING_OPS = {\"$contains\", \"$not_contains\", \"$regex\", \"$not_regex\"}\n\ndef is_valid_where_document(wd: object) -> bool:\n    if not isinstance(wd, dict) or len(wd) != 1:\n        return False\n    op, operand = next(iter(wd.items()))\n    if op in STRING_OPS:\n        return isinstance(operand, str) and len(operand) > 0\n    if op in (\"$and\", \"$or\"):\n        return isinstance(operand, list) and len(operand) >= 2 and all(is_valid_where_document(e) for e in operand)\n    return False","tryCatchPattern":"try:\n    res = collection.get(where_document=wd)\nexcept ValueError as e:\n    if \"to be a str, got\" in str(e):\n        raise ValueError(f\"bad where_document operand: {wd!r}\") from e\n    raise","preventionTips":["Remember there is no implicit list-OR: multiple terms need $or with $contains clauses","Type-annotate filter builders (dict[str, str]) so mypy catches non-str operands","Validate deserialized request payloads before forwarding them as filters"],"tags":["chromadb","where-document","type-mismatch","input-validation"],"backgroundTag":"query-filter-validation","analyzedSha":"aecdd12c8a891610db8653630b066b32ceb678b5","analyzedAt":"2026-08-16T21:53:27.228Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}