From 85ab9cb85d973762d0f65b4966dfedc5d9fafdf0 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Sun, 18 Oct 2026 08:57:49 +0200 Subject: [PATCH 1/3] CAMEL-25746: camel-bean, camel-platform-http - the errors for a key read from a stream, JSON text or a List, and for a Map or List as the HTTP response, say what to do In ten benchmark runs of a local model, 3 of the 8 failed steps of the contract-first openapi example hit errors that named the problem but not the fix: a variable set from the raw request body before the unmarshal, a key read from the whole list, and a List returned as the response. - "Key: x not found in bean" adds a hint: for a stream or bytes, unmarshal it first (a variable keeps the value it had when it was set); for JSON text, unmarshal it first; for a List, pick an element such as [0][x], or split it. - platform-http: a Map or List as the response body says to marshal it to JSON first, as the http producer says for a request body (CAMEL-25309). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m --- .../camel/language/bean/BeanExpression.java | 25 ++++++++++++- .../camel/language/simple/SimpleTest.java | 35 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java b/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java index bdc6d8a8757e7..424af338a6b4d 100644 --- a/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java +++ b/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java @@ -16,6 +16,8 @@ */ package org.apache.camel.language.bean; +import java.io.InputStream; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -29,6 +31,7 @@ import org.apache.camel.NoSuchBeanException; import org.apache.camel.Predicate; import org.apache.camel.RuntimeCamelException; +import org.apache.camel.StreamCache; import org.apache.camel.TypeConverter; import org.apache.camel.component.bean.BeanComponent; import org.apache.camel.component.bean.BeanConstants; @@ -687,11 +690,31 @@ private static Object lookupResult( throw new IndexOutOfBoundsException( "Key: " + key + " not found in bean: " + bean + " of type: " + ObjectHelper.classCanonicalName(bean) + " using OGNL path [" + ognlPath - + "]"); + + "]" + keyHint(result != null ? result : bean, key)); } else { // null safe so we can return null return null; } } + /** + * What to do when a key is read from a value that has no keys (CAMEL-25746): the message body as a stream or bytes + * (a request body before it is unmarshalled, or a variable set from it), JSON text, or a list. + */ + static String keyHint(Object value, String key) { + if (value instanceof StreamCache || value instanceof InputStream || value instanceof byte[]) { + return " (the value is the message body as a stream, not a Map: unmarshal it first, for example with json," + + " then read the key; a variable keeps the value it had when it was set)"; + } + if (value instanceof CharSequence text) { + String trimmed = text.toString().strip(); + if (trimmed.startsWith("{") || trimmed.startsWith("[")) { + return " (the value is JSON text, not a Map: unmarshal it first, for example with json, then read the key)"; + } + } + if (value instanceof Collection || value instanceof Object[]) { + return " (a List has no keys: pick an element first, such as [0][" + key + "], or split the list)"; + } + return ""; + } } diff --git a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java index 3e29ee9a5e566..b0f83535de73f 100644 --- a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java @@ -16,6 +16,7 @@ */ package org.apache.camel.language.simple; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -521,6 +522,40 @@ public void testOGNLPropertyMapNotMap() { cause.getMessage()); } + @Test + public void testOGNLKeyOnAStreamSaysToUnmarshal() { + // CAMEL-25746: the request body as a stream, before it is unmarshalled + exchange.getIn().setBody(new ByteArrayInputStream("{\"qty\": 2}".getBytes(StandardCharsets.UTF_8))); + RuntimeBeanExpressionException e = assertThrows(RuntimeBeanExpressionException.class, + () -> assertExpression("${body[qty]}", null)); + + IndexOutOfBoundsException cause = assertIsInstanceOf(IndexOutOfBoundsException.class, e.getCause()); + assertTrue(cause.getMessage().startsWith("Key: qty not found in bean: "), cause.getMessage()); + assertTrue(cause.getMessage().contains("the value is the message body as a stream, not a Map: unmarshal it first"), + cause.getMessage()); + } + + @Test + public void testOGNLKeyOnJsonTextSaysToUnmarshal() { + exchange.getIn().setHeader("order", "{\"qty\": 2}"); + RuntimeBeanExpressionException e = assertThrows(RuntimeBeanExpressionException.class, + () -> assertExpression("${header.order[qty]}", null)); + + IndexOutOfBoundsException cause = assertIsInstanceOf(IndexOutOfBoundsException.class, e.getCause()); + assertTrue(cause.getMessage().contains("the value is JSON text, not a Map: unmarshal it first"), cause.getMessage()); + } + + @Test + public void testOGNLKeyOnAListSaysToPickAnElement() { + exchange.getIn().setBody(List.of(Map.of("sku", "CAMEL-MUG", "qty", 42))); + RuntimeBeanExpressionException e = assertThrows(RuntimeBeanExpressionException.class, + () -> assertExpression("${body[qty]}", null)); + + IndexOutOfBoundsException cause = assertIsInstanceOf(IndexOutOfBoundsException.class, e.getCause()); + assertTrue(cause.getMessage().contains("a List has no keys: pick an element first, such as [0][qty]"), + cause.getMessage()); + } + @Test public void testOGNLPropertyMapIllegalSyntax() { ExpressionIllegalSyntaxException e = assertThrows(ExpressionIllegalSyntaxException.class, From 0a58d0a8f854e3a37809d29561c0f98df7cccc2f Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Sun, 18 Oct 2026 09:20:22 +0200 Subject: [PATCH 2/3] CAMEL-25746: camel-bean - the key hint names the type: a List, an array or a Collection Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m --- .../apache/camel/language/bean/BeanExpression.java | 6 ++++-- .../apache/camel/language/simple/SimpleTest.java | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java b/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java index 424af338a6b4d..13b48a96f9ba3 100644 --- a/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java +++ b/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java @@ -699,7 +699,8 @@ private static Object lookupResult( /** * What to do when a key is read from a value that has no keys (CAMEL-25746): the message body as a stream or bytes - * (a request body before it is unmarshalled, or a variable set from it), JSON text, or a list. + * (a request body before it is unmarshalled, or a variable set from it), JSON text, or a List, array or other + * Collection, where [0] picks an element. */ static String keyHint(Object value, String key) { if (value instanceof StreamCache || value instanceof InputStream || value instanceof byte[]) { @@ -713,7 +714,8 @@ static String keyHint(Object value, String key) { } } if (value instanceof Collection || value instanceof Object[]) { - return " (a List has no keys: pick an element first, such as [0][" + key + "], or split the list)"; + String type = value instanceof List ? "a List" : value instanceof Object[] ? "an array" : "a Collection"; + return " (" + type + " has no keys: pick an element first, such as [0][" + key + "], or split it)"; } return ""; } diff --git a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java index b0f83535de73f..9ebb4de8394c4 100644 --- a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java @@ -556,6 +556,19 @@ public void testOGNLKeyOnAListSaysToPickAnElement() { cause.getMessage()); } + @Test + public void testOGNLKeyOnAnArraySaysToPickAnElement() { + exchange.getIn().setBody(new Object[] { Map.of("sku", "CAMEL-MUG", "qty", 42) }); + RuntimeBeanExpressionException e = assertThrows(RuntimeBeanExpressionException.class, + () -> assertExpression("${body[qty]}", null)); + + IndexOutOfBoundsException cause = assertIsInstanceOf(IndexOutOfBoundsException.class, e.getCause()); + assertTrue(cause.getMessage().contains("an array has no keys: pick an element first, such as [0][qty]"), + cause.getMessage()); + // [0] picks the element the hint points to + assertExpression("${body[0][qty]}", 42); + } + @Test public void testOGNLPropertyMapIllegalSyntax() { ExpressionIllegalSyntaxException e = assertThrows(ExpressionIllegalSyntaxException.class, From 9d52ae3b1a45ccc715b7a1c196c2c4e3d0736dfc Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Sun, 18 Oct 2026 18:12:46 +0200 Subject: [PATCH 3/3] CAMEL-25746: camel-bean - name the hint for a value without keys noKeysHint, beside the keyHint of a Map main now has Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Claus Ibsen --- .../java/org/apache/camel/language/bean/BeanExpression.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java b/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java index 13b48a96f9ba3..9158f62e694d9 100644 --- a/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java +++ b/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java @@ -690,7 +690,7 @@ private static Object lookupResult( throw new IndexOutOfBoundsException( "Key: " + key + " not found in bean: " + bean + " of type: " + ObjectHelper.classCanonicalName(bean) + " using OGNL path [" + ognlPath - + "]" + keyHint(result != null ? result : bean, key)); + + "]" + noKeysHint(result != null ? result : bean, key)); } else { // null safe so we can return null return null; @@ -702,7 +702,7 @@ private static Object lookupResult( * (a request body before it is unmarshalled, or a variable set from it), JSON text, or a List, array or other * Collection, where [0] picks an element. */ - static String keyHint(Object value, String key) { + static String noKeysHint(Object value, String key) { if (value instanceof StreamCache || value instanceof InputStream || value instanceof byte[]) { return " (the value is the message body as a stream, not a Map: unmarshal it first, for example with json," + " then read the key; a variable keeps the value it had when it was set)";