{"record":{"id":"45e6626054fe5058","repo":"kestra-io/kestra","slug":"queryfilter-must-be-either-a-leaf-field-operati","errorCode":null,"errorMessage":"QueryFilter must be either a leaf (field + operation) or a node (logical + non-empty children), not both or neither","messagePattern":"QueryFilter must be either a leaf \\(field \\+ operation\\) or a node \\(logical \\+ non-empty children\\), not both or neither","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":400,"severity":"error","filePath":"core/src/main/java/io/kestra/core/models/QueryFilter.java","lineNumber":40,"sourceCode":"public record QueryFilter(\n    Field field,\n    Op operation,\n    Object value,\n    Logical logical,\n    List<QueryFilter> children) {\n\n    @JsonCreator\n    public QueryFilter(\n        @JsonProperty(\"field\") Field field,\n        @JsonProperty(\"operation\") Op operation,\n        @JsonProperty(\"value\") Object value,\n        @JsonProperty(\"logical\") Logical logical,\n        @JsonProperty(\"children\") List<QueryFilter> children) {\n        boolean leafShape = field != null && operation != null && logical == null && children == null;\n        boolean nodeShape = logical != null && children != null && !children.isEmpty()\n            && field == null && operation == null && value == null;\n        if (!leafShape && !nodeShape) {\n            throw new IllegalArgumentException(\n                \"QueryFilter must be either a leaf (field + operation) or a node (logical + non-empty children), not both or neither\"\n            );\n        }\n        this.field = field;\n        this.operation = operation;\n        this.value = value;\n        this.logical = logical;\n        this.children = children;\n    }\n\n    public boolean isLeaf() {\n        return logical == null;\n    }\n\n    public boolean isNode() {\n        return logical != null;\n    }\n","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/kestra-io/kestra/blob/823fada9274c4f9c251ea0a516460a4f7d958032/core/src/main/java/io/kestra/core/models/QueryFilter.java#L22-L58","documentation":"Thrown by the QueryFilter JsonCreator constructor when the supplied JSON does not match exactly one of two permitted shapes: a 'leaf' (field + operation set, logical and children null) or a 'node' (logical + non-empty children set, field/operation/value null). Mixing the two, supplying a node with empty children, or omitting required fields all trip this guard. It is the structural invariant that lets the query builder safely distinguish composite filters from terminal predicates.","triggerScenarios":"POSTing a dashboard/query filter JSON that includes both 'field' and 'logical'; providing a 'logical' with a null or empty 'children' array; providing 'field' without 'operation'; providing 'children' but no 'logical'.","commonSituations":"Frontend builds a filter object dynamically and leaves stray keys; API client serializes a partial filter; a migration of an old filter shape leaves inconsistent fields.","solutions":["Ensure leaf filters contain ONLY field, operation, and value (no logical/children).","Ensure composite nodes contain ONLY logical and a non-empty children array (no field/operation/value).","Validate the filter JSON shape client-side before sending.","If migrating old filter shapes, normalize them into the leaf-or-node structure first."],"exampleFix":"// before\n{\"field\":\"namespace\",\"operation\":\"EQUAL_TO\",\"value\":\"prod\",\"logical\":\"AND\",\"children\":[]}\n// IllegalArgumentException\n\n// after (leaf)\n{\"field\":\"namespace\",\"operation\":\"EQUAL_TO\",\"value\":\"prod\"}\n// after (node)\n{\"logical\":\"AND\",\"children\":[{\"field\":\"namespace\",\"operation\":\"EQUAL_TO\",\"value\":\"prod\"}]}","handlingStrategy":"validation","validationCode":"boolean leaf = field != null && operation != null && logical == null && (children == null || children.isEmpty());\nboolean node = logical != null && children != null && !children.isEmpty() && field == null && operation == null && value == null;\nif (!leaf && !node) {\n    throw new IllegalArgumentException(\"Invalid QueryFilter shape\");\n}","typeGuard":"// Type guard narrowing a raw filter object\nfunction isLeafFilter(f): f is { field: string; operation: string; value: unknown } {\n  return !!f.field && !!f.operation && !f.logical && !f.children;\n}\nfunction isNodeFilter(f): f is { logical: string; children: unknown[] } {\n  return !!f.logical && Array.isArray(f.children) && f.children.length > 0 && !f.field && !f.operation;\n}","tryCatchPattern":"try {\n    new QueryFilter(field, operation, value, logical, children);\n} catch (IllegalArgumentException e) {\n    // return 400 with the structural requirement\n    throw new InvalidQueryFiltersException(List.of(e.getMessage()));\n}","preventionTips":["Build filters with a factory that enforces leaf-or-node invariants.","Never partially populate both shapes in API payloads."],"tags":["query","validation","json","dashboard","filter"],"backgroundTag":null,"analyzedSha":"823fada9274c4f9c251ea0a516460a4f7d958032","analyzedAt":"2026-08-14T06:15:17.947Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}