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

  1. Wrap every node in an object: {"or":[{"state":"open"},{"state":"closed"}]} not ["state","open"].
  2. For 'not', supply a single object: {"not":{"state":"open"}}.
  3. 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

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


AI-assisted analysis of makeplane/plane@1c8a60f858 (2026-08-12). Data as JSON: /api/errors/bc415bb84e7df2e3. Report an issue: GitHub.