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

  1. Ensure every AND/OR/NOT child is a complete comparison expression (key OP value) or a Group, never a bare operand.
  2. Build filters with the Filter expression builder API (Filter.expr / Filter.builder) instead of manual constructor calls so the tree is validated by construction.
  3. Add a pre-check that walks the filter tree and asserts operands at logical-operator positions are Expression/Group.
  4. 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

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


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/7edec897df3e9018. Report an issue: GitHub.