flowable/flowable-engine · error · ELException
error.compare.types
error.compare.types
Error message
error.compare.types
What it means
BooleanOperations.lt0 performs '<' comparison on two operands after numeric coercion attempts fail. If neither operand is Comparable (and types are otherwise incomparable), it throws an ELException with 'error.compare.types' naming both classes.
Source
Thrown at modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/de/odysseus/el/misc/BooleanOperations.java:65
if (SIMPLE_FLOAT_TYPES.contains(t1) || SIMPLE_FLOAT_TYPES.contains(t2)) {
return converter.convert(o1, Double.class) < converter.convert(o2, Double.class);
}
if (BigInteger.class.isAssignableFrom(t1) || BigInteger.class.isAssignableFrom(t2)) {
return converter.convert(o1, BigInteger.class).compareTo(converter.convert(o2, BigInteger.class)) < 0;
}
if (SIMPLE_INTEGER_TYPES.contains(t1) || SIMPLE_INTEGER_TYPES.contains(t2)) {
return converter.convert(o1, Long.class) < converter.convert(o2, Long.class);
}
if (t1 == String.class || t2 == String.class) {
return converter.convert(o1, String.class).compareTo(converter.convert(o2, String.class)) < 0;
}
if (o1 instanceof Comparable) {
return ((Comparable)o1).compareTo(o2) < 0;
}
if (o2 instanceof Comparable) {
return ((Comparable)o2).compareTo(o1) > 0;
}
throw new ELException(LocalMessages.get("error.compare.types", o1.getClass(), o2.getClass()));
}
@SuppressWarnings("unchecked")
private static final boolean gt0(TypeConverter converter, Object o1, Object o2) {
Class<?> t1 = o1.getClass();
Class<?> t2 = o2.getClass();
if (BigDecimal.class.isAssignableFrom(t1) || BigDecimal.class.isAssignableFrom(t2)) {
return converter.convert(o1, BigDecimal.class).compareTo(converter.convert(o2, BigDecimal.class)) > 0;
}
if (SIMPLE_FLOAT_TYPES.contains(t1) || SIMPLE_FLOAT_TYPES.contains(t2)) {
return converter.convert(o1, Double.class) > converter.convert(o2, Double.class);
}
if (BigInteger.class.isAssignableFrom(t1) || BigInteger.class.isAssignableFrom(t2)) {
return converter.convert(o1, BigInteger.class).compareTo(converter.convert(o2, BigInteger.class)) > 0;
}
if (SIMPLE_INTEGER_TYPES.contains(t1) || SIMPLE_INTEGER_TYPES.contains(t2)) {
return converter.convert(o1, Long.class) > converter.convert(o2, Long.class);
}View on GitHub (pinned to d6d39ce1c6)
Solutions
- Make one of the operand types implement Comparable (with matching comparison semantics).
- Coerce operands in the expression to comparable types (e.g. numbers or strings) before comparing.
- Rewrite the expression condition to compare a comparable field instead of the objects themselves.
Example fix
// before: #{user < otherUser} (POJOs, not Comparable)
// after: #{user.id < otherUser.id}
// or make User implement Comparable<User> with compareTo comparing by id Defensive patterns
Strategy: type-guard
Validate before calling
boolean comparable(Object o) { return o == null || o instanceof Comparable || o instanceof Number || o instanceof String; }
if (!comparable(a) || !comparable(b)) throw new IllegalArgumentException("operands of < must be Comparable"); Type guard
boolean isComparableOperand(Object o) {
return o instanceof Comparable || o instanceof Number || o instanceof String;
} Try / catch
try {
boolean result = BooleanOperations.lt(converter, a, b);
} catch (ELException e) {
// incomparable operand types; coerce or compare a comparable field instead
} Prevention
- Ensure types used in <, >, <=, >= EL comparisons implement Comparable.
- Compare scalar fields (id, date, amount) rather than whole objects.
- Keep bean property types stable (Comparable) across refactors.
When it happens
Trigger: Evaluating an EL comparison like a < b where a and b are non-numeric, non-Comparable objects (e.g. arbitrary POJOs, arrays), via lt() or ge().
Common situations: Comparing unrelated object types in an expression condition; a bean property's type changed between versions so a formerly Comparable value is now a POJO; comparing booleans or custom types without Comparable support.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- error.negate
- Invalid usage of async_activate job handler, variable scope
- Invalid usage of ${TYPE} job handler, variable scope is of t
- Delegate expression ${expression} did not resolve to an impl
- Delegate expression ${expression} did not resolve to an impl
AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11).
Data as JSON: /api/errors/2fe2e37345f0e537.
Report an issue: GitHub.