makeplane/plane · error · DRFValidationError
invalid_filter_node
invalid_filter_node
Error message
Each filter node must be a JSON object
What it means
Raised by _validate_structure when a node in the filter tree is not a Python dict. Every node — whether a logical operator container, a not operand, a child of or/and, or a leaf — must be a JSON object; JSON arrays or scalars at a node position are rejected.
Source
Thrown at apps/api/plane/utils/filters/filter_backend.py:335
- Each object may contain only one logical operator:
or/and/not (case-insensitive)
- Logical operator objects cannot contain field keys alongside the
operator
- or/and values must be non-empty lists of dicts
- not value must be a dict
- Leaf objects must only contain field keys and acceptable values
- Depth must not exceed max_depth
"""
if current_depth > max_depth:
raise DRFValidationError(
{
"message": (f"Filter nesting is too deep (max {max_depth}); found depth {current_depth}"),
"code": "max_depth_exceeded",
}
)
if not isinstance(node, dict):
raise DRFValidationError(
{
"message": "Each filter node must be a JSON object",
"code": "invalid_filter_node",
}
)
if not node:
raise DRFValidationError(
{
"message": "Filter objects must not be empty",
"code": "empty_filter_object",
}
)
logical_keys = [k for k in node.keys() if isinstance(k, str) and k.lower() in ("or", "and", "not")]
if len(logical_keys) > 1:
raise DRFValidationError(View on GitHub (pinned to 1c8a60f858)
Solutions
- Wrap every node in an object: {"or":[{"state":"open"},{"state":"closed"}]} not ["state","open"].
- For 'not', supply a single object: {"not":{"state":"open"}}.
- Validate the top-level shape with a JSON-schema checker on the client before sending.
Example fix
// before
?filters={"or":["state","priority"]}
// after
?filters={"or":[{"state":"open"},{"priority":"high"}]} Defensive patterns
Strategy: type-guard
Validate before calling
def assert_all_nodes_are_dicts(node):
if not isinstance(node, dict):
raise ValueError(f'expected object, got {type(node).__name__}')
for k, v in node.items():
if isinstance(k, str) and k.lower() in ('or', 'and'):
for c in v:
assert_all_nodes_are_dicts(c)
elif isinstance(k, str) and k.lower() == 'not':
assert_all_nodes_are_dicts(v) Type guard
function isFilterNode(v) {
if (typeof v !== 'object' || v === null || Array.isArray(v)) return false;
return true;
} Prevention
- Top-level filter must be an object, not an array.
- or/and children must be objects; wrap bare strings/fields before pushing them.
When it happens
Trigger: GET .../?filters=["state","open"] (top-level array); GET .../?filters={"or":["state","open"]} (children of or are strings); GET .../?filters={"not":["state"]} (not operand is a list instead of an object).
Common situations: Client treats the filter as a flat list of field names; misunderstanding that 'or'/'and' take a list of *objects* not strings; misreading the schema after migrating from a legacy filter format.
Related errors
- empty_filter_object
- multiple_logical_operators
- mixed_operator_and_fields
- invalid_operator_children
- invalid_operator_child_type
AI-assisted analysis of makeplane/plane@1c8a60f858 (2026-08-12).
Data as JSON: /api/errors/bc415bb84e7df2e3.
Report an issue: GitHub.