makeplane/plane · error · DRFValidationError

empty_filter_object

empty_filter_object

Error message

Filter objects must not be empty

What it means

Raised by _validate_structure when a node is an empty dict {}. Empty operator containers ({'and':[]}) are caught earlier as invalid_operator_children; this fires when the node itself has no keys, e.g. a stray {} in an or/and list or a top-level empty filter object that survived _normalize_filter_data (which only checks for falsy at line 82 and returns queryset).

Source

Thrown at apps/api/plane/utils/filters/filter_backend.py:343

        """
        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(
                {
                    "message": ("A filter object cannot contain multiple logical operators at the same level"),
                    "code": "multiple_logical_operators",
                }
            )

        if len(logical_keys) == 1:
            op_key = logical_keys[0]

View on GitHub (pinned to 1c8a60f858)

Solutions

  1. Remove empty objects from the filter tree before sending.
  2. In the client rule builder, disable submit while any group has zero clauses.
  3. If a group is intentionally a no-op, drop it rather than emitting {}.

Example fix

// before
?filters={"or":[{"state":"open"},{}]}
// after
?filters={"or":[{"state":"open"}]}
Defensive patterns

Strategy: validation

Validate before calling

def strip_empty_nodes(node):
    if not isinstance(node, dict):
        return node
    if not node:
        raise ValueError('empty filter object encountered')
    return {k: (strip_empty_nodes(v) if isinstance(v, dict)
               else [strip_empty_nodes(c) for c in v] if isinstance(v, list)
               else v)
            for k, v in node.items()}

Type guard

function hasNoEmptyObjects(node) {
  if (typeof node !== 'object' || node === null) return true;
  if (Array.isArray(node)) return node.every(hasNoEmptyObjects);
  const keys = Object.keys(node);
  if (keys.length === 0) return false;
  return keys.every(k => hasNoEmptyObjects(node[k]));
}

Prevention

When it happens

Trigger: GET .../?filters={} is short-circuited earlier (returns unfiltered queryset), but GET .../?filters={"or":[{}]} or {"not":{}} reaches _validate_structure on the empty inner dict and triggers this.

Common situations: Programmatic builders that push empty group placeholders; serialization libraries that emit {} for null sub-conditions; UI rule builder that allows adding a group with no clauses yet.

Related errors


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