Claus Ibsen created CAMEL-24844:
-----------------------------------

             Summary: Body type flow: the validator carries the body's Java 
type from step to step and warns where a step assumes text; the message 
history's recorded body types feed the same check at runtime
                 Key: CAMEL-24844
                 URL: https://issues.apache.org/jira/browse/CAMEL-24844
             Project: Camel
          Issue Type: Improvement
          Components: camel-jbang, camel-yaml-dsl, camel-core
            Reporter: Claus Ibsen


Camel carries the payload as the natural Java object: a GenericFile from the 
file consumer, a Map or List after {{unmarshal json}}, a byte[] or a 
StreamCache after {{marshal}}, an InputStream from http. Integration people 
think in text and bytes, and write routes as if the body were a String. Every 
failure in the round-2 local-model benchmark that was not a YAML shape error 
was that collision, and a person writing the same route gets the same runtime 
exception:

* {{${body.orderId}}} after {{unmarshal: json}} (the body is a Map): 
MethodNotFoundException, in five examples. The runtime message now says "the 
value is a Map: a key is read with [orderId]".
* {{body.email}} in a Groovy expression on a GenericFile, before any 
conversion: MissingPropertyException.
* {{jsonpath}} on a body that is already a Map after unmarshal, or on a null 
body on a timer route (CAMEL-24838).
* {{${body}}} after {{unmarshal: json}} treated as JSON text: it is the Map's 
toString ({{{id=ORD-1001, ...}}}).
* {{new ByteArrayInputStream(body)}} in Groovy after {{marshal}}: with stream 
caching on (the Camel CLI default) the body is a StreamCache, not a byte[]: 
"could not find matching constructor". Same route, different Java type 
depending on a runtime setting.

Measured with the CLI on 4.23.0-SNAPSHOT: logging {{${body}}} prints readable 
text at every stage (GenericFile, Map, marshal output, InputStream with stream 
caching on, twice); only with {{camel.main.streamCachingEnabled=false}} does a 
log step consume an InputStream and leave every later step an empty body.

The object model must stay: it is why a Map flows into a bean and a stream into 
a file without copies. What can change is that Camel knows the type at every 
step and says nothing until the step that assumes text explodes.

Proposal, in two halves that share one rule set (which step produces which 
type, which expressions and steps need which type):

# *Static, in {{camel validate yaml}} and the camel-jbang-mcp validation tool.* 
Walk the route and carry the body type forward from the catalog: {{from: file}} 
gives GenericFile, {{unmarshal: json}} gives Map/List (or the unmarshalType), 
{{marshal}} gives byte[]/StreamCache, {{unmarshal: jacksonXml}} gives Map, 
{{split}} on a List gives an element, {{setBody: constant}} gives String, {{to: 
http}} gives InputStream, {{convertBodyTo}} gives its type, a bean gives its 
return type when it can be resolved. At each step check the expression against 
the type and say it in the words of the text world, with the line: "after 
unmarshal json the body is a Map: ${body.orderId} fails, write 
${body[orderId]}"; "jsonpath needs the JSON text, move it before the unmarshal 
or use simple on the Map"; "the body is the file (GenericFile): convert it with 
convertBodyTo String or unmarshal it before the Groovy expression reads 
body.email"; "after marshal the body is bytes (a cached stream when stream 
caching is on): a Groovy script gets it with exchange.message.getBody(byte[])". 
Unknown types (a bean with no source) stop the flow silently, no false 
positives.
# *At runtime, from the message history.* The message history already records, 
per step, what went through, including the class of the body at each previous 
step. Use it in two places: (a) the exception message of a failing expression 
or bean invocation can say "the body reaching this step was a 
java.util.LinkedHashMap (set by unmarshal at line 12)", the fact a person 
otherwise gets from a debugger; (b) {{camel trace}} and the camel-jbang-mcp 
tools that show a message's history can show the body type per step, so the 
flow is visible on a running app, and the static half can be checked against it 
(a recorded history of the reference run is the ground truth for the catalog's 
type rules).

The static half is where most of the value is (it reaches the file before it 
runs); the runtime half makes the rule set honest and gives the "what is the 
body here" answer on a live app.

Context: the round-2 local-model benchmark on the camel-jbang-examples ladder 
(2026-09-19); the pattern was in aggregator, order-lines, csv-to-json, groovy, 
openapi-client, filter-and-multicast, json-transform.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to