Summary
After the Jackson 3 migration (#430, PR #431), the comparison (<, >, <=, >=) and equality (=, in) operators can throw a raw, uncaught tools.jackson.databind.node.JsonNodeException instead of returning a value. In Jackson 2 the underlying asDouble()/asLong() calls coerced non‑numeric strings to 0.0 and silently narrowed out‑of‑range integers; Jackson 3 made these coercions fail‑fast (they throw). The affected call sites are unguarded and their lines were not changed by the migration, so the behavior flipped from "return a boolean" to "crash".
This was not caught by the test suite — no existing test exercises these mismatched/out‑of‑range operand combinations.
Where
src/main/java/com/api/jsonata4java/expressions/ExpressionsVisitor.java
areJsonNodesEqual (drives = and in): lines 170–177 — left.asDouble() == right.asDouble() and left.asLong() == right.asLong().
< operator: lines 1327/1329 (asDouble), 1331/1333 (asLong).
> operator: lines 1354/1356, 1358/1360.
<= operator: lines 1381/1383, 1385/1387.
>= operator: lines 1408/1410, 1411/1414.
In every case the branch is entered as soon as one side is floating‑point (or both are integral), with no check that the other side is actually numeric / in range.
Reproductions
| Expression |
Jackson 2 result |
Jackson 3 result |
3.14 = "abc" |
false (valid JSONata) |
throws JsonNodeException |
3.14 in ["abc"] |
false |
throws |
3.14 < "abc" |
false |
throws |
99999999999999999999 < 5 (integer literal > Long.MAX_VALUE) |
compared narrowed value |
throws |
The 3.14 = "abc" case is the most serious: comparing a number to a non‑numeric string for equality is legal JSONata and must yield false, not raise an exception.
Verified empirically against jackson‑databind 3.2.0
StringNode("abc").asDouble() -> THROWS JsonNodeException
StringNode("abc").asLong() -> THROWS JsonNodeException
BigIntegerNode(99999999999999999999).asLong() -> THROWS JsonNodeException
Suggested fix
Guard each numeric branch so the throwing coercion is only reached for actually‑coercible operands (e.g. require both sides numeric before asDouble()/asLong(), or range‑check with canConvertToLong() and fall back to doubleValue()/bigIntegerValue()), mirroring the guards already added to MatchFunction/ReplaceFunction/ArrayUtils.compare in this migration. Where operand types are genuinely incompatible, throw the controlled EvaluateRuntimeException the code already uses for the string/type‑mismatch branches rather than leaking a Jackson exception.
Summary
After the Jackson 3 migration (#430, PR #431), the comparison (
<,>,<=,>=) and equality (=,in) operators can throw a raw, uncaughttools.jackson.databind.node.JsonNodeExceptioninstead of returning a value. In Jackson 2 the underlyingasDouble()/asLong()calls coerced non‑numeric strings to0.0and silently narrowed out‑of‑range integers; Jackson 3 made these coercions fail‑fast (they throw). The affected call sites are unguarded and their lines were not changed by the migration, so the behavior flipped from "return a boolean" to "crash".This was not caught by the test suite — no existing test exercises these mismatched/out‑of‑range operand combinations.
Where
src/main/java/com/api/jsonata4java/expressions/ExpressionsVisitor.javaareJsonNodesEqual(drives=andin): lines 170–177 —left.asDouble() == right.asDouble()andleft.asLong() == right.asLong().<operator: lines 1327/1329 (asDouble), 1331/1333 (asLong).>operator: lines 1354/1356, 1358/1360.<=operator: lines 1381/1383, 1385/1387.>=operator: lines 1408/1410, 1411/1414.In every case the branch is entered as soon as one side is floating‑point (or both are integral), with no check that the other side is actually numeric / in range.
Reproductions
3.14 = "abc"false(valid JSONata)JsonNodeException3.14 in ["abc"]false3.14 < "abc"false99999999999999999999 < 5(integer literal >Long.MAX_VALUE)The
3.14 = "abc"case is the most serious: comparing a number to a non‑numeric string for equality is legal JSONata and must yieldfalse, not raise an exception.Verified empirically against jackson‑databind 3.2.0
Suggested fix
Guard each numeric branch so the throwing coercion is only reached for actually‑coercible operands (e.g. require both sides numeric before
asDouble()/asLong(), or range‑check withcanConvertToLong()and fall back todoubleValue()/bigIntegerValue()), mirroring the guards already added toMatchFunction/ReplaceFunction/ArrayUtils.comparein this migration. Where operand types are genuinely incompatible, throw the controlledEvaluateRuntimeExceptionthe code already uses for the string/type‑mismatch branches rather than leaking a Jackson exception.