[
https://issues.apache.org/jira/browse/CAMEL-24844?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18118369#comment-18118369
]
Claus Ibsen commented on CAMEL-24844:
-------------------------------------
Phase A and phase B in PR https://github.com/apache/camel/pull/26800
{{RouteGraph}} builds the route topology from the source - the same graph
{{DefaultRouteTopologyDumper}} builds from the definitions at runtime - and the
walk over it reports a body-reading step only when every way into its route
provably carries none. Phase B reads the OpenAPI specification beside the
route, so a GET operation of a {{rest: openApi:}} binding is known to carry no
body, which is the shape phase A had to stay silent on.
Measured over real files:
|| corpus || files || reports ||
| routes a model wrote against an OpenAPI contract | 208 | 12, each the missing
read that made the route answer 500 |
| camel-kamelets | 997 | 0 |
| camel-jbang-examples, camel-examples, benchmark output | 2010 | 0 |
Running it over those files rather than over fixtures found two bugs in the
walk that fixtures would not have: a step whose nested branches set the body is
not a step without one, and an endpoint written as {{uri: direct}} with
{{parameters: {name: lookup}}} is the same endpoint as {{direct:lookup}} - the
runtime topology never meets that form because the uri is resolved by the time
it sees it.
h3. What is not in it
* the type lattice: this pass answers whether there is a body, not what type it
is. The rules in the proposal - a Map after {{unmarshal json}}, bytes after
{{marshal}}, an element after {{split}} - build on the same graph and come next.
* a YAML OpenAPI specification: only JSON is read.
* the runtime half: {{DefaultMessageHistory}} already records {{bodyType}} and
{{bodySize}} per step, so the exception message and the history tools can say
what the body was and where it came from, and a recorded run is the ground
truth the static rules can be checked against.
h3. What the graph gives beyond this check
Two things that need no types at all and are decidable in one directory:
* {{to: direct:lookup}} with no route consuming it - a startup failure today
({{No consumers available on endpoint}}), and with {{seda:}} no failure at all,
the message is queued and nothing reads it
* a {{direct:}} route that nothing calls, which is what a whole-file rewrite
leaves behind
Written and tested already, held back from the PR until the scan of the sibling
files is there, since a file on its own cannot answer either question.
And the report itself: a command that reads a set of files and prints the
topology with what flows over it would serve a person and an agent equally -
the graph is the same one drawn by the topology dev console, but available
before the application runs.
> 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-core, camel-jbang, camel-yaml-dsl
> Reporter: Claus Ibsen
> Assignee: Claus Ibsen
> Priority: Major
>
> 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)