This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch CAMEL-25928-r22-body-type-gaps
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 762c8c46d8b8a304e46fa7af4cea2ab85ebc22b0
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 26 09:37:43 2026 +0100

    CAMEL-25928: camel-support, camel-bean, camel-groovy - runtime hints for an 
unmarshal of data, a field read after a marshal, and a file in a variable
    
    - An unmarshal of a Map or a List says the body (or the variable) is data 
already, instead of the bare type converter
      error.
    - A field read on text that a marshal wrote from parsed data says to read 
the field before the marshal, as the message
      history tells, instead of "unmarshal it first".
    - A key read on the payload of the body gets one hint, from what the body 
is, not a second one that disagrees.
    - ${variable.customer.name} on a variable that holds the file as read says 
to unmarshal it in place, as the [name]
      form already did.
    - The Groovy hint "still text ... unmarshal it first" is given only when 
the body is text: on parsed data it says the
      value read is a text value of the data.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/language/bean/BeanExpression.java | 23 +++++++++--
 .../camel/language/groovy/GroovyExpression.java    | 10 +++++
 .../groovy/GroovyMissingPropertyHintTest.java      | 26 ++++++++++++-
 .../language/simple/SimpleSyntaxHintsTest.java     | 18 +++++++++
 .../apache/camel/language/simple/SimpleTest.java   |  4 +-
 .../camel/processor/UnmarshalProcessorTest.java    | 37 ++++++++++++++++++
 .../org/apache/camel/support/BodyFactsTest.java    | 36 +++++++++++++++++
 .../java/org/apache/camel/support/BodyFacts.java   | 45 +++++++++++++++++++++-
 .../support/processor/UnmarshalProcessor.java      | 31 ++++++++++++++-
 9 files changed, 221 insertions(+), 9 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 1e51db17bef9..8a83f96c09f8 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
@@ -571,11 +571,15 @@ public class BeanExpression implements Expression, 
Predicate {
             Object bean = beanOf(holder, exchange);
             String body = bodyHint(exchange, bean);
             String list = body == null ? listHint(bean, methodName) : null;
+            // ${variable.customer.name} on a variable that holds the file as 
read
+            String file = body == null && (bean instanceof WrappedFile<?> || 
bean instanceof File)
+                    ? noKeysHint(bean, methodName) : null;
             throw new RuntimeBeanExpressionException(
                     exchange, describeBean(holder, beanName, exchange),
                     body != null
                             ? method + " (" + body + ")"
-                            : list != null ? method + list : keyHint(holder, 
exchange, methodName, method),
+                            : list != null ? method + list
+                            : file != null ? method + file : keyHint(holder, 
exchange, methodName, method),
                     resultExchange.getException());
         }
         return newResult;
@@ -692,8 +696,12 @@ public class BeanExpression implements Expression, 
Predicate {
         }
         if (key != null) {
             Object bean = holder.getBean(exchange);
+            // the payload of the message body is told by bodyHint below, from 
what the message history knows
+            Object value = result != null ? result : bean;
+            boolean payloadBody = bean != null && bean == 
exchange.getMessage().getBody() && value == bean
+                    && isPayload(value);
             try {
-                return lookupResult(resultExchange, key, result, nullSafe, 
ognlPath, bean);
+                return lookupResult(resultExchange, key, result, nullSafe, 
ognlPath, bean, !payloadBody);
             } catch (IndexOutOfBoundsException e) {
                 // ${body[status]} on the file as read, or on a stream: the 
same exception, saying what the body is
                 // and the step that parses it (CAMEL-25749)
@@ -730,8 +738,15 @@ public class BeanExpression implements Expression, 
Predicate {
         }
     }
 
+    /** Text, bytes, a stream or the file as read: what an unmarshal turns 
into data. */
+    private static boolean isPayload(Object value) {
+        return value instanceof WrappedFile<?> || value instanceof File || 
value instanceof StreamCache
+                || value instanceof InputStream || value instanceof byte[] || 
value instanceof String;
+    }
+
     private static Object lookupResult(
-            Exchange exchange, String key, Object result, boolean nullSafe, 
String ognlPath, Object bean) {
+            Exchange exchange, String key, Object result, boolean nullSafe, 
String ognlPath, Object bean,
+            boolean hint) {
         StringHelper.notEmpty(key, "key", "in Simple language ognl path: " + 
ognlPath);
 
         final TypeConverter typeConverter = 
exchange.getContext().getTypeConverter();
@@ -786,7 +801,7 @@ public class BeanExpression implements Expression, 
Predicate {
             throw new IndexOutOfBoundsException(
                     "Key: " + key + " not found in bean: " + bean + " of type: 
"
                                                 + 
ObjectHelper.classCanonicalName(bean) + " using OGNL path [" + ognlPath
-                                                + "]" + noKeysHint(result != 
null ? result : bean, key));
+                                                + "]" + (hint ? 
noKeysHint(result != null ? result : bean, key) : ""));
         } else {
             // null safe so we can return null
             return null;
diff --git 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
index 1631050895c6..7cee45ee440c 100644
--- 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
+++ 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
@@ -34,6 +34,7 @@ import org.apache.camel.Message;
 import org.apache.camel.RuntimeCamelException;
 import org.apache.camel.attachment.AttachmentMessage;
 import org.apache.camel.attachment.DefaultAttachmentMessage;
+import org.apache.camel.spi.BodyTypeRegistry;
 import org.apache.camel.support.BodyFacts;
 import org.apache.camel.support.ExchangeHelper;
 import org.apache.camel.support.ExpressionSupport;
@@ -107,6 +108,10 @@ public class GroovyExpression extends ExpressionSupport {
             // (CAMEL-25749); null for an object, where the name read is wrong
             String payload = !onText && body != null && e.getType() == 
body.getClass() && !(body instanceof Map)
                     ? BodyFacts.hint(exchange, BodyFacts.Need.DATA) : null;
+            // customer.customerNo on parsed data, where customer holds the 
text C-482: the text is a value of the
+            // data, and the body needs no unmarshal
+            BodyTypeRegistry.Kind kind = BodyFacts.kindOf(body);
+            boolean bodyData = kind == BodyTypeRegistry.Kind.DATA || kind == 
BodyTypeRegistry.Kind.COLLECTION;
             String hint;
             if (bodyText) {
                 // what the body is, the step that set it, and the unmarshal 
that parses it (CAMEL-25749)
@@ -119,6 +124,11 @@ public class GroovyExpression extends ExpressionSupport {
                 hint = "header '" + textHeader + "' is still JSON text, not 
parsed data: parse it in the script with"
                        + " new groovy.json.JsonSlurper().parseText(" + ref + 
"), or set the header from the body after"
                        + " unmarshal: json";
+            } else if (onText && bodyData && !bean) {
+                String keys = BodyFacts.hint(exchange, BodyFacts.Need.DATA);
+                hint = "the value read here is text (a " + 
e.getType().getSimpleName() + ") in the body, not an object"
+                       + " with fields: " + BodyFacts.describe(exchange) + ", 
which is data already, so no unmarshal is"
+                       + " needed; check the name read, and the value it is 
read on" + (keys != null ? " (" + keys + ")" : "");
             } else if (onText && !bean) {
                 hint = "the value read here is still text (a " + 
e.getType().getSimpleName()
                        + " comes from iterating text), not parsed data: 
unmarshal it first (unmarshal: json for JSON),"
diff --git 
a/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
 
b/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
index b24bbd5b4d71..346246d740c3 100644
--- 
a/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
+++ 
b/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
@@ -18,6 +18,8 @@ package org.apache.camel.processor.groovy;
 
 import java.io.ByteArrayInputStream;
 import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.Map;
 
 import groovy.lang.MissingPropertyException;
 import org.apache.camel.Exchange;
@@ -28,6 +30,7 @@ import org.apache.camel.test.junit6.CamelTestSupport;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -139,12 +142,30 @@ public class GroovyMissingPropertyHintTest extends 
CamelTestSupport {
 
     @Test
     public void fieldReadOnTextElsewhereSaysItIsText() {
-        // a list of plain text values: an element is text, not data with 
fields
+        // a list of plain text values: an element is text, not data with 
fields, and the body needs no unmarshal
         Exception e = assertThrows(Exception.class,
                 () -> template.sendBodyAndHeader("direct:field", 
java.util.List.of("A1", "B2"), "sku", "A1"));
         MissingPropertyException cause = 
assertInstanceOf(MissingPropertyException.class, e.getCause());
-        assertTrue(cause.getMessage().contains("the value read here is still 
text (a String comes from iterating text)"),
+        assertTrue(cause.getMessage().contains("the value read here is text (a 
String) in the body, not an object with"
+                                               + " fields: the body is a 
List"),
                 cause.getMessage());
+        assertTrue(cause.getMessage().contains("which is data already, so no 
unmarshal is needed"), cause.getMessage());
+        assertFalse(cause.getMessage().contains("unmarshal it first"), 
cause.getMessage());
+    }
+
+    @Test
+    public void fieldReadOnATextFieldOfParsedDataSaysTheBodyIsParsed() {
+        // what the benchmark wrote (r22): customer holds the customer number, 
and customer.customerNo reads it
+        Exception e = assertThrows(Exception.class,
+                () -> template.sendBody("direct:customer", new 
LinkedHashMap<>(Map.of("customer", "C-482"))));
+        MissingPropertyException cause = 
assertInstanceOf(MissingPropertyException.class, e.getCause());
+        assertTrue(cause.getMessage().contains("No such property: customerNo 
for class: java.lang.String"),
+                cause.getMessage());
+        assertTrue(cause.getMessage().contains("the value read here is text (a 
String) in the body, not an object with"
+                                               + " fields: the body is a Map 
(LinkedHashMap), which is data already"),
+                cause.getMessage());
+        assertTrue(cause.getMessage().contains("(its keys are: customer)"), 
cause.getMessage());
+        assertFalse(cause.getMessage().contains("unmarshal it first"), 
cause.getMessage());
     }
 
     @Test
@@ -169,6 +190,7 @@ public class GroovyMissingPropertyHintTest extends 
CamelTestSupport {
                 
from("direct:unknown").transform().groovy("nosuch.toUpperCase()");
                 from("direct:field").transform().groovy("body.find { it.sku == 
headers.sku }");
                 from("direct:read").transform().groovy("body.sku");
+                
from("direct:customer").transform().groovy("body.customer.customerNo");
                 from("direct:header").transform().groovy("headers.skuList.find 
{ it.sku == headers.sku }");
                 
from("direct:message").transform().groovy("message.getHeader('name') + ' ' + 
message.body");
             }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
index e8c6096db880..22239384ac3c 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
@@ -324,6 +324,24 @@ public class SimpleSyntaxHintsTest extends 
ExchangeTestSupport {
                 .contains("unmarshal: {json: {}, variableSend: customer, 
variableReceive: customer}");
     }
 
+    @Test
+    public void testMethodOfAVariableThatHoldsAFileSaysToUnmarshalIt() {
+        // what the benchmark wrote (r22): ${variable.customer.name} reads the 
polled file, not the customer
+        GenericFile<File> file = new GenericFile<>();
+        file.setFile(new File("customers/C-482.json"));
+        file.setFileName("C-482.json");
+        exchange.setVariable("customer", file);
+        Exception e = assertThrows(Exception.class,
+                () -> 
context.resolveLanguage("simple").createExpression("${variable.customer.name}").evaluate(exchange,
+                        String.class));
+        assertThat(e.getMessage()).contains("(the value is the file as read (a 
GenericFile), not its data: unmarshal it"
+                                            + " first")
+                .contains("unmarshal: {json: {}, variableSend: customer, 
variableReceive: customer}");
+        // what the file has is read as it is
+        assertEquals("C-482.json", 
context.resolveLanguage("simple").createExpression("${variable.customer.fileName}")
+                .evaluate(exchange, String.class));
+    }
+
     @Test
     public void testOgnlOnAnObjectSaysNothingAboutTheBody() {
         // a name an object does not have is a wrong name, not a body of the 
wrong type
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 1ec074ef84cc..d4d38598eaf4 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
@@ -531,8 +531,10 @@ public class SimpleTest extends LanguageTestSupport {
 
         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"),
+        // what the body is says it once: no second hint about the value
+        assertTrue(cause.getMessage().contains("(the body is a stream 
(ByteArrayInputStream): unmarshal it first"),
                 cause.getMessage());
+        assertFalse(cause.getMessage().contains("the value is the message body 
as a stream"), cause.getMessage());
     }
 
     @Test
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/UnmarshalProcessorTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/UnmarshalProcessorTest.java
index 57c4c9d906c1..d6ac1b06f24c 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/processor/UnmarshalProcessorTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/UnmarshalProcessorTest.java
@@ -18,6 +18,10 @@ package org.apache.camel.processor;
 
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.Exchange;
@@ -135,6 +139,39 @@ public class UnmarshalProcessorTest extends TestSupport {
                 exchange.getException().getMessage());
     }
 
+    @Test
+    public void testParsedDataSaysItIsDataAlready() throws Exception {
+        // what the benchmark wrote (r22): the order was unmarshalled, and is 
unmarshalled again
+        Exchange exchange = createExchangeWithBody(new DefaultCamelContext(), 
new LinkedHashMap<>(Map.of("orderId", "1")));
+        Processor processor = new UnmarshalProcessor(new MyDataFormat(new 
Object()));
+
+        processor.process(exchange);
+
+        NoTypeConversionAvailableException e
+                = assertInstanceOf(NoTypeConversionAvailableException.class, 
exchange.getException());
+        assertEquals("Cannot unmarshal using MyDataFormat: the body is a Map 
(LinkedHashMap), which is data already, and"
+                     + " the data format reads text: read its fields as they 
are, and leave this unmarshal out; to parse the"
+                     + " text in a variable, unmarshal it with variableSend 
and variableReceive set to its name",
+                e.getMessage());
+        assertEquals(InputStream.class, e.getToType());
+        assertInstanceOf(NoTypeConversionAvailableException.class, 
e.getCause());
+    }
+
+    @Test
+    public void testParsedDataInAVariableSaysItIsDataAlready() throws 
Exception {
+        Exchange exchange = createExchangeWithBody(new DefaultCamelContext(), 
"body");
+        exchange.setVariable("order", new ArrayList<>(List.of("A1")));
+        UnmarshalProcessor processor = new UnmarshalProcessor(new 
MyDataFormat(new Object()));
+        processor.setVariableSend("order");
+
+        processor.process(exchange);
+
+        assertEquals("Cannot unmarshal using MyDataFormat: the variable order 
holds data (ArrayList) already, and the data"
+                     + " format reads text: read its fields as they are, such 
as ${variable.order[name]}, and leave this"
+                     + " unmarshal out",
+                exchange.getException().getMessage());
+    }
+
     @Test
     public void testBodyThatCannotBeConvertedKeepsTheConverterMessage() throws 
Exception {
         Exchange exchange = createExchangeWithBody(new DefaultCamelContext(), 
new Object());
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/support/BodyFactsTest.java 
b/core/camel-core/src/test/java/org/apache/camel/support/BodyFactsTest.java
index 35b821925785..04490b33e180 100644
--- a/core/camel-core/src/test/java/org/apache/camel/support/BodyFactsTest.java
+++ b/core/camel-core/src/test/java/org/apache/camel/support/BodyFactsTest.java
@@ -17,6 +17,8 @@
 package org.apache.camel.support;
 
 import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -30,7 +32,9 @@ import org.apache.camel.ContextTestSupport;
 import org.apache.camel.Exchange;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.spi.BodyTypeRegistry.Kind;
+import org.apache.camel.spi.DataFormat;
 import org.apache.camel.support.BodyFacts.Need;
+import org.apache.camel.support.service.ServiceSupport;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -59,6 +63,8 @@ public class BodyFactsTest extends ContextTestSupport {
                 from("direct:unchanged").process(e -> 
seen.add(BodyFacts.describe(e)));
                 from("direct:setBy").setBody(constant(new 
LinkedHashMap<>(Map.of("sku", "A1")))).log("next")
                         .process(e -> seen.add(BodyFacts.describe(e)));
+                from("direct:marshalled").marshal(new TextDataFormat())
+                        .process(e -> seen.add(BodyFacts.describe(e) + ": " + 
BodyFacts.hint(e, Need.DATA)));
             }
         };
     }
@@ -76,6 +82,22 @@ public class BodyFactsTest extends ContextTestSupport {
         assertTrue(seen.get(0).startsWith("the body is a Map (LinkedHashMap), 
set by setBody"), seen.get(0));
     }
 
+    @Test
+    public void testAFieldReadAfterAMarshalOfData() {
+        // what the benchmark wrote (r22): the ticket is marshalled for a 
file, and a field is read after it
+        template.sendBody("direct:marshalled", new 
LinkedHashMap<>(Map.of("ticketId", "T-1")));
+        assertEquals(1, seen.size());
+        assertTrue(seen.get(0).contains(", set by marshal"), seen.get(0));
+        assertTrue(seen.get(0).contains("wrote it from data (LinkedHashMap): 
read the field before the marshal, for"
+                                        + " example into a header with 
setHeader, or keep the data in the body and"
+                                        + " marshal into a variable"),
+                seen.get(0));
+        // a marshal of text: unmarshal was meant
+        seen.clear();
+        template.sendBody("direct:marshalled", "{\"ticketId\": \"T-1\"}");
+        assertTrue(seen.get(0).contains("unmarshal it first"), seen.get(0));
+    }
+
     @Test
     public void testTheBodyInPlainWords() {
         assertEquals("null", BodyFacts.words(null));
@@ -191,6 +213,20 @@ public class BodyFactsTest extends ContextTestSupport {
         assertNull(BodyFacts.dataFormatOf(null));
     }
 
+    /** Writes the body as its text. */
+    private static final class TextDataFormat extends ServiceSupport 
implements DataFormat {
+
+        @Override
+        public void marshal(Exchange exchange, Object graph, OutputStream 
stream) throws Exception {
+            
stream.write(String.valueOf(graph).getBytes(StandardCharsets.UTF_8));
+        }
+
+        @Override
+        public Object unmarshal(Exchange exchange, InputStream stream) {
+            throw new UnsupportedOperationException();
+        }
+    }
+
     private Exchange exchange(Object body) {
         Exchange answer = new DefaultExchange(context);
         answer.getMessage().setBody(body);
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/BodyFacts.java 
b/core/camel-support/src/main/java/org/apache/camel/support/BodyFacts.java
index ef8584393b57..44153b6ed59c 100644
--- a/core/camel-support/src/main/java/org/apache/camel/support/BodyFacts.java
+++ b/core/camel-support/src/main/java/org/apache/camel/support/BodyFacts.java
@@ -72,6 +72,12 @@ import org.apache.camel.util.URISupport;
  * </tr>
  * <tr>
  * <td>{@link Need#DATA}</td>
+ * <td>text, bytes or a stream a marshal wrote from a Map or a List, as the 
message history says</td>
+ * <td>read the field before the marshal, or marshal into a variable: an 
unmarshal would undo the marshal a later step
+ * needs</td>
+ * </tr>
+ * <tr>
+ * <td>{@link Need#DATA}</td>
  * <td>null, or empty text or bytes</td>
  * <td>as for {@link Need#A_BODY}</td>
  * </tr>
@@ -214,7 +220,8 @@ public final class BodyFacts {
             return readTheDataFirst();
         }
         if (isPayload(body)) {
-            return unmarshalFirst(exchange, body);
+            String marshal = marshalledData(exchange, body);
+            return marshal != null ? marshal : unmarshalFirst(exchange, body);
         }
         if (body instanceof Map<?, ?> map) {
             if (map.isEmpty()) {
@@ -277,6 +284,42 @@ public final class BodyFacts {
         };
     }
 
+    /**
+     * When a marshal wrote the body from a Map or a List, as the message 
history says: the marshal is wanted, for a
+     * later step that needs text (a file write), and the field is read after 
it, too late; an unmarshal would undo it.
+     * Null when the history does not say so, as for a marshal of text, where 
an unmarshal was meant.
+     */
+    private static String marshalledData(Exchange exchange, Object body) {
+        List<MessageHistory> history = 
exchange.getProperty(ExchangePropertyKey.MESSAGE_HISTORY, List.class);
+        if (history == null || history.isEmpty()) {
+            return null;
+        }
+        String now = ObjectHelper.classCanonicalName(body);
+        for (int i = history.size() - 1; i >= 0; i--) {
+            MessageHistory step = history.get(i);
+            String type = step.getBodyType();
+            if (type == null || type.equals(now)) {
+                continue;
+            }
+            // the step that set the body, and the body it was reached with
+            if (step.getNode() == null || 
!"marshal".equals(step.getNode().getShortName())
+                    || !isDataType(exchange.getContext(), type)) {
+                return null;
+            }
+            return "the " + step(step.getNode()) + " wrote it from data (" + 
StringHelper.afterLast(type, ".", type)
+                   + "): read the field before the marshal, for example into a 
header with setHeader, or keep the data"
+                   + " in the body and marshal into a variable (marshal with 
variableReceive, and send that variable with"
+                   + " variableSend)";
+        }
+        return null;
+    }
+
+    /** Whether a class, by its name, is a Map or a collection. */
+    private static boolean isDataType(CamelContext context, String type) {
+        Class<?> clazz = context != null ? 
context.getClassResolver().resolveClass(type) : null;
+        return clazz != null && (Map.class.isAssignableFrom(clazz) || 
Collection.class.isAssignableFrom(clazz));
+    }
+
     private static String readTheDataFirst() {
         return "read the data first - setBody with constant: 
resource:file:data.json for a known file, or poll:,"
                + " pollEnrich or a from: consumer for one that is not (a route 
reached with direct: has the body of its"
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/processor/UnmarshalProcessor.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/processor/UnmarshalProcessor.java
index 056c8fb13bf0..f34374e4099c 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/processor/UnmarshalProcessor.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/processor/UnmarshalProcessor.java
@@ -17,7 +17,9 @@
 package org.apache.camel.support.processor;
 
 import java.io.InputStream;
+import java.util.Collection;
 import java.util.Iterator;
+import java.util.Map;
 
 import org.apache.camel.AsyncCallback;
 import org.apache.camel.CamelContext;
@@ -124,7 +126,7 @@ public class UnmarshalProcessor extends 
AsyncProcessorSupport
         } catch (Exception e) {
             // remove OUT message, as an exception occurred
             exchange.setOut(null);
-            exchange.setException(body == null ? nullBodyException(exchange, 
e) : e);
+            exchange.setException(body == null ? nullBodyException(exchange, 
e) : parsedDataException(exchange, body, e));
         } finally {
             // The Iterator will close the stream itself
             if (!(result instanceof Iterator)) {
@@ -158,6 +160,33 @@ public class UnmarshalProcessor extends 
AsyncProcessorSupport
         return answer;
     }
 
+    /**
+     * A Map or a List fails in the type converter too, as the data format 
reads text: "No type converter available to
+     * convert from type: java.util.LinkedHashMap to the required type: 
java.io.InputStream". Say what it means here:
+     * the body, or the variable, is data already, and a variable is 
unmarshalled with variableSend and variableReceive.
+     * The exception type stays the same, so an onException for it still 
matches.
+     */
+    private Exception parsedDataException(Exchange exchange, Object body, 
Exception e) {
+        if (!(e instanceof NoTypeConversionAvailableException ntc) || !(body 
instanceof Map) && !(body instanceof Collection)) {
+            return e;
+        }
+        String name
+                = dataFormat instanceof DataFormatName dfn ? 
dfn.getDataFormatName() : dataFormat.getClass().getSimpleName();
+        String message = variableSend != null
+                ? "Cannot unmarshal using " + name + ": the variable " + 
variableSend + " holds data ("
+                  + body.getClass().getSimpleName()
+                  + ") already, and the data format reads text: read its 
fields as they are, such as"
+                  + " ${variable." + variableSend + "[name]}, and leave this 
unmarshal out"
+                : "Cannot unmarshal using " + name + ": " + 
BodyFacts.describe(exchange)
+                  + ", which is data already, and the data format reads text: 
read its fields as they are, and leave"
+                  + " this unmarshal out; to parse the text in a variable, 
unmarshal it with variableSend and"
+                  + " variableReceive set to its name";
+        NoTypeConversionAvailableException answer
+                = new NoTypeConversionAvailableException(message, body, 
ntc.getToType());
+        answer.initCause(e);
+        return answer;
+    }
+
     @Override
     public String toString() {
         return "Unmarshal[" + dataFormat + "]";

Reply via email to