{"record":{"id":"5bc3f8218ae09893","repo":"makeplane/plane","slug":"invalid-leaf","errorCode":"invalid_leaf","errorMessage":"Leaf filter must be a non-empty JSON object","messagePattern":"Leaf filter must be a non-empty JSON object","errorType":"validation","errorClass":"DRFValidationError","httpStatus":400,"severity":"error","filePath":"apps/api/plane/utils/filters/filter_backend.py","lineNumber":414,"sourceCode":"\n            if op == \"not\":\n                if not isinstance(value, dict):\n                    raise DRFValidationError(\n                        {\n                            \"message\": \"'not' must be a single JSON object\",\n                            \"code\": \"invalid_not_child\",\n                        }\n                    )\n                self._validate_structure(value, max_depth=max_depth, current_depth=current_depth + 1)\n                return\n\n        # Leaf node: validate fields and values\n        self._validate_leaf(node)\n\n    def _validate_leaf(self, leaf):\n        \"\"\"Validate a leaf dict containing field lookups and values.\"\"\"\n        if not isinstance(leaf, dict) or not leaf:\n            raise DRFValidationError(\n                {\n                    \"message\": \"Leaf filter must be a non-empty JSON object\",\n                    \"code\": \"invalid_leaf\",\n                }\n            )\n\n        for key, value in leaf.items():\n            if isinstance(key, str) and key.lower() in (\"or\", \"and\", \"not\"):\n                raise DRFValidationError(\n                    {\n                        \"message\": \"Logical operators cannot appear in a leaf filter object\",\n                        \"code\": \"operator_in_leaf\",\n                    }\n                )\n\n            # Lists/Tuples must contain only scalar values\n            if isinstance(value, (list, tuple)):\n                if len(value) == 0:","sourceCodeStart":396,"sourceCodeEnd":432,"githubUrl":"https://github.com/makeplane/plane/blob/1c8a60f858d8472aa56e29994ec1c7926da2c6ce/apps/api/plane/utils/filters/filter_backend.py#L396-L432","documentation":"Raised by _validate_leaf when the leaf node (an object with no or/and/not keys, reached at the end of _validate_structure) is not a dict or is an empty dict. Leaves must be non-empty JSON objects whose keys are field names; everything else is a structural error.","triggerScenarios":"Structurally unreachable in the default pipeline because _validate_structure already rejects non-dict nodes (invalid_filter_node) and empty nodes (empty_filter_object) before dispatching to _validate_leaf. Can fire only in a subclass that calls _validate_leaf directly with bad input, or if _validate_structure is overridden to skip those checks.","commonSituations":"Subclassing ComplexFilterBackend and invoking _validate_leaf from custom code; unit tests that exercise _validate_leaf in isolation without preparing a proper dict.","solutions":["Route all nodes through _validate_structure rather than calling _validate_leaf directly.","If you must call _validate_leaf, ensure the argument is a non-empty dict first.","Keep the structural pre-checks intact when subclassing."],"exampleFix":"# before\nself._validate_leaf(None)\n\n# after\nif isinstance(node, dict) and node and not _is_operator_node(node):\n    self._validate_leaf(node)","handlingStrategy":"validation","validationCode":"def assert_leaf_ok(leaf):\n    if not isinstance(leaf, dict) or not leaf:\n        raise ValueError('leaf filter must be a non-empty object')","typeGuard":"function isNonEmptyObject(v) {\n  return typeof v === 'object' && v !== null && !Array.isArray(v) && Object.keys(v).length > 0;\n}","tryCatchPattern":null,"preventionTips":["Do not call _validate_leaf directly; let _validate_structure dispatch to it.","Preserve the structural pre-checks when subclassing ComplexFilterBackend."],"tags":["filters","schema","validation","api"],"backgroundTag":null,"analyzedSha":"1c8a60f858d8472aa56e29994ec1c7926da2c6ce","analyzedAt":"2026-08-12T14:44:31.636Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}