spring-projects/spring-ai · error · IllegalArgumentException
Unsupported operand type:
Error message
Unsupported operand type:
What it means
S3VectorStoreFilterExpressionEvaluator.evaluateOperand recursively evaluates Filter operands against row metadata and only understands Filter.Group and Filter.Expression. A bare Filter.Key or Filter.Value operand at an expression position is a malformed filter tree, so it throws IllegalArgumentException naming the operand class.
Source
Thrown at vector-stores/spring-ai-s3-vector-store/src/main/java/org/springframework/ai/vectorstore/s3/S3VectorStoreFilterExpressionEvaluator.java:52
* @author Jewoo Shin
*/
final class S3VectorStoreFilterExpressionEvaluator {
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
.withZone(ZoneOffset.UTC);
boolean evaluate(Filter.Expression expression, Map<String, Object> metadata) {
return evaluateExpression(expression, metadata);
}
private boolean evaluateOperand(Filter.Operand operand, Map<String, Object> metadata) {
if (operand instanceof Filter.Group group) {
return evaluateOperand(group.content(), metadata);
}
if (operand instanceof Filter.Expression expression) {
return evaluateExpression(expression, metadata);
}
throw new IllegalArgumentException("Unsupported operand type: " + operand.getClass().getName());
}
private boolean evaluateExpression(Filter.Expression expression, Map<String, Object> metadata) {
return switch (expression.type()) {
case AND -> evaluateOperand(left(expression), metadata) && evaluateOperand(right(expression), metadata);
case OR -> evaluateOperand(left(expression), metadata) || evaluateOperand(right(expression), metadata);
case NOT -> !evaluateOperand(left(expression), metadata);
case EQ -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) == 0;
case NE -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) != 0;
case GT -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) > 0;
case GTE -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) >= 0;
case LT -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) < 0;
case LTE -> compare(metadataValue(left(expression), metadata), filterValue(right(expression))) <= 0;
case IN -> {
Object metaVal = metadataValue(left(expression), metadata);
List<?> list = asList(filterValue(right(expression)), expression);
yield list.stream().anyMatch(item -> compare(metaVal, item) == 0);
}View on GitHub (pinned to 98a7beda4f)
Solutions
- Ensure every AND/OR/NOT child is a complete comparison expression (key OP value) or a Group, never a bare operand.
- Build filters with the Filter expression builder API (Filter.expr / Filter.builder) instead of manual constructor calls so the tree is validated by construction.
- Add a pre-check that walks the filter tree and asserts operands at logical-operator positions are Expression/Group.
- If you control the code path, catch IllegalArgumentException around evaluate and fail fast with a clearer message.
Example fix
// before
new Filter.Expression(Filter.ExpressionType.AND, new Filter.Key("a"), eq("b", 1))
// after
new Filter.Expression(Filter.ExpressionType.AND, eq("a", 1), eq("b", 1)) Defensive patterns
Strategy: validation
Validate before calling
static void validateOperands(Filter.Expression e) {
for (Filter.Operand o : new Filter.Operand[]{ e.left(), e.right() }) {
if (o == null) continue;
if (o instanceof Filter.Expression x) validateOperands(x);
else if (!(o instanceof Filter.Group) && e.type() == AND || e.type() == OR)
throw new IllegalArgumentException("Bare operand under logical operator: " + o.getClass());
}
} Type guard
static boolean isLogicalOperand(Filter.Operand o) { return o instanceof Filter.Expression || o instanceof Filter.Group; } Try / catch
try { store.similaritySearch(req); } catch (IllegalArgumentException e) { if (e.getMessage().contains("Unsupported operand type")) { /* rebuild filter tree */ } } Prevention
- Build filters with Filter.expr(...) or the builder API instead of manual constructors.
- Every AND/OR child must be a full comparison or Group.
- Unit-test programmatic filter assembly with a tree validator.
When it happens
Trigger: Hand-building a Filter tree where an AND/OR child operand is a plain Key or Value instead of an Expression/Group, then running S3VectorStore similaritySearch that triggers post-filter evaluation of ListVectors metadata.
Common situations: Programmatic filter construction mistakes (missing comparison around a key); serialized/persisted filters from another store that don't round-trip to valid expressions; reflection-based filter builders.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Expression of type %s requires a right operand
- Expected a Key operand but got:
- Expected a Value operand but got:
- Cannot compare values of incompatible types %s and %s
- Not supported expression type:
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/7edec897df3e9018.
Report an issue: GitHub.