kestra-io/kestra · error · IllegalArgumentException

QueryFilter must be either a leaf (field + operation) or a n

Error message

QueryFilter must be either a leaf (field + operation) or a node (logical + non-empty children), not both or neither

What it means

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.

Source

Thrown at core/src/main/java/io/kestra/core/models/QueryFilter.java:40

public record QueryFilter(
    Field field,
    Op operation,
    Object value,
    Logical logical,
    List<QueryFilter> children) {

    @JsonCreator
    public QueryFilter(
        @JsonProperty("field") Field field,
        @JsonProperty("operation") Op operation,
        @JsonProperty("value") Object value,
        @JsonProperty("logical") Logical logical,
        @JsonProperty("children") List<QueryFilter> children) {
        boolean leafShape = field != null && operation != null && logical == null && children == null;
        boolean nodeShape = logical != null && children != null && !children.isEmpty()
            && field == null && operation == null && value == null;
        if (!leafShape && !nodeShape) {
            throw new IllegalArgumentException(
                "QueryFilter must be either a leaf (field + operation) or a node (logical + non-empty children), not both or neither"
            );
        }
        this.field = field;
        this.operation = operation;
        this.value = value;
        this.logical = logical;
        this.children = children;
    }

    public boolean isLeaf() {
        return logical == null;
    }

    public boolean isNode() {
        return logical != null;
    }

View on GitHub (pinned to 823fada927)

Solutions

  1. Ensure leaf filters contain ONLY field, operation, and value (no logical/children).
  2. Ensure composite nodes contain ONLY logical and a non-empty children array (no field/operation/value).
  3. Validate the filter JSON shape client-side before sending.
  4. If migrating old filter shapes, normalize them into the leaf-or-node structure first.

Example fix

// before
{"field":"namespace","operation":"EQUAL_TO","value":"prod","logical":"AND","children":[]}
// IllegalArgumentException

// after (leaf)
{"field":"namespace","operation":"EQUAL_TO","value":"prod"}
// after (node)
{"logical":"AND","children":[{"field":"namespace","operation":"EQUAL_TO","value":"prod"}]}
Defensive patterns

Strategy: validation

Validate before calling

boolean leaf = field != null && operation != null && logical == null && (children == null || children.isEmpty());
boolean node = logical != null && children != null && !children.isEmpty() && field == null && operation == null && value == null;
if (!leaf && !node) {
    throw new IllegalArgumentException("Invalid QueryFilter shape");
}

Type guard

// Type guard narrowing a raw filter object
function isLeafFilter(f): f is { field: string; operation: string; value: unknown } {
  return !!f.field && !!f.operation && !f.logical && !f.children;
}
function isNodeFilter(f): f is { logical: string; children: unknown[] } {
  return !!f.logical && Array.isArray(f.children) && f.children.length > 0 && !f.field && !f.operation;
}

Try / catch

try {
    new QueryFilter(field, operation, value, logical, children);
} catch (IllegalArgumentException e) {
    // return 400 with the structural requirement
    throw new InvalidQueryFiltersException(List.of(e.getMessage()));
}

Prevention

When it happens

Trigger: 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'.

Common situations: 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.

Related errors


AI-assisted analysis of kestra-io/kestra@823fada927 (2026-08-14). Data as JSON: /api/errors/45e6626054fe5058. Report an issue: GitHub.