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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 1e8f70977f83 CAMEL-25928: camel-tooling-model - one rule to read a 
body type, and a Message Body Types page (#28437)
1e8f70977f83 is described below

commit 1e8f70977f8398b1ee0bdda3f22cf55477c46b34
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 25 12:49:54 2026 +0100

    CAMEL-25928: camel-tooling-model - one rule to read a body type, and a 
Message Body Types page (#28437)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../modules/ROOT/partials/body-types.adoc          |   2 +-
 .../ROOT/images/body-types-route-diagram.svg       | 104 ++++++
 .../jbang/camel-tui-source-body-type-problem.png   | Bin 0 -> 255685 bytes
 docs/user-manual/modules/ROOT/nav.adoc             |   1 +
 .../user-manual/modules/ROOT/pages/body-types.adoc | 409 +++++++++++++++++++++
 .../modules/ROOT/pages/camel-catalog.adoc          | 106 +-----
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |   2 +-
 .../modules/ROOT/pages/camel-jbang-validation.adoc |   2 +-
 .../modules/ROOT/pages/endpoint-annotations.adoc   |   4 +-
 .../camel/dsl/yaml/validator/CatalogBodyTypes.java |  85 +----
 .../camel/tooling/model/BodyTypeResolver.java      | 177 +++++++++
 .../camel/tooling/model/BodyTypeResolverTest.java  | 160 ++++++++
 .../camel/maven/packaging/BodyTypesHelper.java     |  18 +-
 13 files changed, 872 insertions(+), 198 deletions(-)

diff --git a/docs/components/modules/ROOT/partials/body-types.adoc 
b/docs/components/modules/ROOT/partials/body-types.adoc
index 8df01700d940..a79f7abeeb2b 100644
--- a/docs/components/modules/ROOT/partials/body-types.adoc
+++ b/docs/components/modules/ROOT/partials/body-types.adoc
@@ -16,6 +16,6 @@ ifeval::[{bodytypecount} != 0]
 
jsonpathTable::{body-types-module}:example$json/{shortname}.json['nodes$.bodyTypes.*','camel.bodySide(path[2])|camel.bodyCarriers(value)|camel.bodyDescription(value)','camel=xref:ROOT:js/camel.js']
 
 The bodies are the ones with the default options. A side that is not listed is 
not known, and tooling does not guess
-it. The carriers are described in the xref:manual::camel-catalog.adoc[Camel 
Catalog].
+it. The carriers, the sides and the tools that use them are described in 
xref:manual::body-types.adoc[Message Body Types].
 endif::[]
 // body types: END
diff --git a/docs/user-manual/modules/ROOT/images/body-types-route-diagram.svg 
b/docs/user-manual/modules/ROOT/images/body-types-route-diagram.svg
new file mode 100644
index 000000000000..d1e70fd74699
--- /dev/null
+++ b/docs/user-manual/modules/ROOT/images/body-types-route-diagram.svg
@@ -0,0 +1,104 @@
+<svg xmlns="http://www.w3.org/2000/svg"; width="760" height="540" viewBox="0 0 
760 540" font-family="'Red Hat Text', 'Helvetica Neue', Helvetica, Arial, 
sans-serif">
+  <title>A route diagram with the body types on the arrows and a warning on 
the step that cannot take the body</title>
+  <defs>
+    <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" 
markerHeight="7" orient="auto-start-reverse">
+      <path d="M 0 0 L 10 5 L 0 10 z" fill="#8a8d90"/>
+    </marker>
+    <marker id="arrow-warn" viewBox="0 0 10 10" refX="9" refY="5" 
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
+      <path d="M 0 0 L 10 5 L 0 10 z" fill="#c58c00"/>
+    </marker>
+    <filter id="shadow" x="-10%" y="-10%" width="120%" height="140%">
+      <feDropShadow dx="0" dy="1" stdDeviation="1.5" flood-color="#000" 
flood-opacity="0.12"/>
+    </filter>
+  </defs>
+
+  <!-- canvas -->
+  <rect x="0" y="0" width="760" height="540" rx="8" fill="#f5f6f7"/>
+  <rect x="0" y="0" width="760" height="36" rx="8" fill="#ffffff"/>
+  <rect x="0" y="28" width="760" height="8" fill="#ffffff"/>
+  <line x1="0" y1="36" x2="760" y2="36" stroke="#d2d2d2"/>
+  <text x="16" y="23" font-size="13" font-weight="600" 
fill="#151515">orders.camel.yaml</text>
+  <text x="160" y="23" font-size="13" fill="#6a6e73">route: orders</text>
+  <g transform="translate(626,10)">
+    <path d="M 8 1 L 15 14 L 1 14 Z" fill="#f0ab00" stroke="#c58c00" 
stroke-width="1" stroke-linejoin="round"/>
+    <text x="8" y="12.5" font-size="9" font-weight="700" fill="#151515" 
text-anchor="middle">!</text>
+    <text x="22" y="13" font-size="12" fill="#6a6e73">1 warning</text>
+  </g>
+
+  <!-- node 1: from file -->
+  <g filter="url(#shadow)">
+    <rect x="80" y="60" width="240" height="60" rx="8" fill="#ffffff" 
stroke="#c7c7c7"/>
+  </g>
+  <circle cx="112" cy="90" r="18" fill="#0066cc"/>
+  <path d="M 105 80 L 115 80 L 120 85 L 120 100 L 105 100 Z M 115 80 L 115 85 
L 120 85" fill="none" stroke="#ffffff" stroke-width="1.6" 
stroke-linejoin="round"/>
+  <text x="142" y="86" font-size="14" font-weight="600" 
fill="#151515">from</text>
+  <text x="142" y="105" font-size="12" fill="#6a6e73">file:inbox</text>
+
+  <!-- edge 1 -->
+  <line x1="200" y1="120" x2="200" y2="168" stroke="#8a8d90" 
stroke-width="1.5" marker-end="url(#arrow)"/>
+  <rect x="212" y="133" width="118" height="22" rx="11" fill="#e7f1fa" 
stroke="#73bcf7"/>
+  <text x="271" y="148" font-size="12" fill="#004080" 
text-anchor="middle">file (GenericFile)</text>
+
+  <!-- node 2: unmarshal json -->
+  <g filter="url(#shadow)">
+    <rect x="80" y="170" width="240" height="60" rx="8" fill="#ffffff" 
stroke="#c7c7c7"/>
+  </g>
+  <circle cx="112" cy="200" r="18" fill="#5e40be"/>
+  <text x="112" y="205.5" font-size="14" font-weight="700" fill="#ffffff" 
text-anchor="middle">{ }</text>
+  <text x="142" y="196" font-size="14" font-weight="600" 
fill="#151515">unmarshal</text>
+  <text x="142" y="215" font-size="12" fill="#6a6e73">json</text>
+
+  <!-- edge 2 -->
+  <line x1="200" y1="230" x2="200" y2="278" stroke="#8a8d90" 
stroke-width="1.5" marker-end="url(#arrow)"/>
+  <rect x="212" y="243" width="104" height="22" rx="11" fill="#f3faf2" 
stroke="#7cc674"/>
+  <text x="264" y="258" font-size="12" fill="#1e4f18" text-anchor="middle">Map 
or List</text>
+
+  <!-- node 3: log -->
+  <g filter="url(#shadow)">
+    <rect x="80" y="280" width="240" height="60" rx="8" fill="#ffffff" 
stroke="#c7c7c7"/>
+  </g>
+  <circle cx="112" cy="310" r="18" fill="#009596"/>
+  <path d="M 103 303 L 121 303 M 103 310 L 121 310 M 103 317 L 115 317" 
stroke="#ffffff" stroke-width="2" stroke-linecap="round"/>
+  <text x="142" y="306" font-size="14" font-weight="600" 
fill="#151515">log</text>
+  <text x="142" y="325" font-size="12" fill="#6a6e73">Order ${body[id]} 
...</text>
+
+  <!-- edge 3: the body that does not fit -->
+  <line x1="200" y1="340" x2="200" y2="388" stroke="#c58c00" stroke-width="2" 
stroke-dasharray="5 3" marker-end="url(#arrow-warn)"/>
+  <rect x="212" y="353" width="104" height="22" rx="11" fill="#fdf7e7" 
stroke="#f0ab00"/>
+  <text x="264" y="368" font-size="12" fill="#795600" text-anchor="middle">Map 
or List</text>
+
+  <!-- node 4: to file, with the warning -->
+  <g filter="url(#shadow)">
+    <rect x="80" y="390" width="240" height="60" rx="8" fill="#ffffff" 
stroke="#f0ab00" stroke-width="2"/>
+  </g>
+  <circle cx="112" cy="420" r="18" fill="#0066cc"/>
+  <path d="M 103 420 L 120 420 M 114 414 L 120 420 L 114 426" fill="none" 
stroke="#ffffff" stroke-width="2" stroke-linecap="round" 
stroke-linejoin="round"/>
+  <text x="142" y="416" font-size="14" font-weight="600" 
fill="#151515">to</text>
+  <text x="142" y="435" font-size="12" fill="#6a6e73">file:outbox</text>
+  <g transform="translate(298,376)">
+    <path d="M 14 1 L 27 24 L 1 24 Z" fill="#f0ab00" stroke="#c58c00" 
stroke-width="1.2" stroke-linejoin="round"/>
+    <text x="14" y="21" font-size="15" font-weight="700" fill="#151515" 
text-anchor="middle">!</text>
+  </g>
+
+  <!-- the callout -->
+  <path d="M 330 392 L 352 384 L 352 400 Z" fill="#fdf7e7" stroke="#f0ab00"/>
+  <g filter="url(#shadow)">
+    <rect x="351" y="300" width="390" height="176" rx="8" fill="#fdf7e7" 
stroke="#f0ab00"/>
+  </g>
+  <rect x="352" y="385" width="3" height="14" fill="#fdf7e7"/>
+  <g transform="translate(368,314)">
+    <path d="M 9 1 L 17 15 L 1 15 Z" fill="#f0ab00" stroke="#c58c00" 
stroke-width="1" stroke-linejoin="round"/>
+    <text x="9" y="13.5" font-size="10" font-weight="700" fill="#151515" 
text-anchor="middle">!</text>
+  </g>
+  <text x="394" y="327" font-size="14" font-weight="600" fill="#151515">The 
body here is parsed data</text>
+  <text x="368" y="352" font-size="12.5" fill="#151515">to: file needs the 
body as text, bytes, a stream or a file,</text>
+  <text x="368" y="370" font-size="12.5" fill="#151515">but unmarshal: json 
turned the text into a Map or a List,</text>
+  <text x="368" y="388" font-size="12.5" fill="#151515">which a file cannot be 
written from.</text>
+  <rect x="368" y="404" width="200" height="28" rx="14" fill="#0066cc"/>
+  <text x="468" y="422.5" font-size="12.5" font-weight="600" fill="#ffffff" 
text-anchor="middle">+ Add marshal: json before</text>
+  <text x="368" y="458" font-size="11.5" fill="#6a6e73">From the body types of 
file, json and the log EIP in the catalog</text>
+
+  <!-- legend -->
+  <text x="80" y="500" font-size="12" fill="#6a6e73">The labels on the arrows 
are the body each step leaves, before the route runs.</text>
+  <text x="80" y="518" font-size="12" fill="#6a6e73">An illustration of what a 
visual route editor can show with the body types.</text>
+</svg>
diff --git 
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-body-type-problem.png
 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-body-type-problem.png
new file mode 100644
index 000000000000..747ff3423aaa
Binary files /dev/null and 
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-body-type-problem.png
 differ
diff --git a/docs/user-manual/modules/ROOT/nav.adoc 
b/docs/user-manual/modules/ROOT/nav.adoc
index 702ca4ebea49..8440caa8c701 100644
--- a/docs/user-manual/modules/ROOT/nav.adoc
+++ b/docs/user-manual/modules/ROOT/nav.adoc
@@ -113,6 +113,7 @@
 ** xref:jmx.adoc[JMX]
 ** xref:key-value-repository.adoc[KeyValueRepository]
 ** xref:lifecycle.adoc[Camel Lifecycle]
+** xref:body-types.adoc[Message Body Types]
 ** xref:message-size.adoc[Message Size]
 ** xref:oncompletion.adoc[OnCompletion]
 ** xref:pluggable-class-resolvers.adoc[Pluggable Class Resolvers]
diff --git a/docs/user-manual/modules/ROOT/pages/body-types.adoc 
b/docs/user-manual/modules/ROOT/pages/body-types.adoc
new file mode 100644
index 000000000000..4e1b7ff4bf6b
--- /dev/null
+++ b/docs/user-manual/modules/ROOT/pages/body-types.adoc
@@ -0,0 +1,409 @@
+= Message Body Types
+
+*Available as of Camel 4.24*
+
+Every step of a route needs some kinds of message body and leaves one. The 
`file` consumer gives the file it read,
+`unmarshal: json` turns text into a `Map` or a `List`, and the `file` producer 
writes text, bytes, a stream or a file,
+but not a `Map`. When the body one step leaves is not a body the next step can 
take, the route fails, but only when a
+message gets there.
+
+Camel describes these bodies in its catalog, as the _body types_ of each 
component, data format, language and EIP.
+They say what each step needs and what it leaves, with its default options and 
with the options that change it. Tools
+read them to tell before the route runs what the body is at each step, and 
where it does not fit.
+
+The body types help you:
+
+* *Find a route that cannot work before it runs.* The YAML validator follows 
the body through the steps of each route,
+  and reports a step that cannot take the body it gets, with a hint of what to 
add.
+* *Know what a step gives without reading its code.* The documentation page of 
each component, data format, language
+  and EIP has a _Message Body_ section, and the Camel CLI and its MCP server 
show the same.
+* *Write routes with AI.* An AI assistant that reads the body types writes 
steps that fit together, such as a
+  `marshal` before a step that sends text.
+* *See what the body was when the routes ran.* With the message history on, 
Camel records the body each step has been
+  reached with. A step reached with bodies of more than one kind is a part of 
the route that needs attention.
+
+== How it works, in a picture
+
+Each step has two body types: the bodies it needs, and the body it leaves. A 
tool lines them up along a route, step by
+step, before the route runs:
+
+[source,text]
+----
+  from: file:inbox
+      │   file          the file consumer gives the file it read
+      ▼
+  unmarshal: json       needs text, bytes, a stream or a file       ✓
+      │   Map or List   the parsed JSON
+      ▼
+  log                   needs any body                              ✓
+      │   Map or List   log leaves the body as it was
+      ▼
+  to: file:outbox       needs text, bytes, a stream or a file       ✗
+                        ⚠ the body is a Map or a List: add marshal: json 
before this step
+----
+
+The same body types tell what the body is where a route is not wrong: after 
`marshal: json` the body is JSON text
+again, which the `file` producer writes.
+
+== Carriers: the kinds of body
+
+A body type names the _carriers_ of the body: the kinds of Java object it can 
be in. They are coarse on purpose: a
+carrier that is right is better than a precise Java type that is wrong.
+
+[width="100%",cols="1,3,1",options="header"]
+|===
+| Carrier | The body is | Kind at runtime
+| `null` | No body. | `null`
+| `text` | A `String`. | `payload`
+| `bytes` | A `byte[]`. | `payload`
+| `stream` | An `InputStream`, a `Reader`, or the stream cache of Camel. | 
`payload`
+| `file` | A file: the `GenericFile` of the file consumers, a `java.io.File` 
or a `Path`. | `payload`
+| `value` | A single value: a number, a boolean or an enum. | `payload`
+| `map` | A `java.util.Map`, such as a JSON object parsed, or a row of a 
query. | `data`
+| `pojo` | Another Java object, such as a DOM `Document` or a class a payload 
is unmarshalled into. | `data`
+| `list` | A `List`, another `Collection` or an array, such as a JSON array 
parsed, or the rows of a query. | `collection`
+| `iterator` | An `Iterator`, read once, element by element, with no size and 
no index. | `collection`
+|===
+
+Three more words have a meaning of their own:
+
+* `unchanged` -- the body after the step is the body before it, such as after 
a `log` or a `to: kafka`.
+* `any` -- the step takes any body, such as a `setHeader`. Only for the bodies 
a step needs.
+* `unknown` -- the body depends on what the other system sends, or on a class 
the user configures. Tools must not guess
+  it. Only for the body after a step.
+
+The _kinds_ are what Camel records at runtime (see 
xref:components:eips:message-history.adoc[Message History]): a
+step reached with bodies of one kind, such as `String` and `byte[]`, is 
dynamic but fine; a step reached with a payload
+for some messages and data for others needs attention.
+
+== Sides: what a step needs and what it leaves
+
+The body types of a model are a set of _sides_. A side that is not listed is 
not known.
+
+[width="100%",cols="1,1,3",options="header"]
+|===
+| Model | Side | The body
+.4+| Component | `consumerOut` | The body of the message the consumer creates: 
what a `from` gives the route.
+| `consumerReply` | The bodies a consumer that answers the caller can reply 
with: what the route can end with.
+| `producerIn` | The bodies the producer can send or write: what a `to` needs.
+| `producerOut` | The body after the producer.
+.4+| Data format | `marshalIn` | The bodies the data format can marshal.
+| `marshalOut` | The body after `marshal`.
+| `unmarshalIn` | The bodies the data format can unmarshal.
+| `unmarshalOut` | The body after `unmarshal`.
+.2+| Language | `expressionIn` | The bodies the expressions of the language 
can read.
+| `expressionOut` | The result of an expression.
+.3+| EIP | `eipIn` | The bodies the EIP needs, such as text for `tokenizer`.
+| `eipOut` | The body after the EIP.
+| `eipSteps` | The body the steps of the EIP start with, such as each part for 
`split`.
+|===
+
+== Examples
+
+=== A step that cannot take the body
+
+This route reads JSON files, parses them, and writes them to another directory:
+
+// yaml-validator: skip
+[source,yaml]
+----
+- route:
+    from:
+      uri: file:inbox
+      steps:
+        - unmarshal:
+            json: {}
+        - to:
+            uri: file:outbox
+----
+
+The `file` consumer gives the file (`consumerOut` is `file`), `unmarshal: 
json` leaves a `Map` or a `List`
+(`unmarshalOut`), and the `file` producer needs text, bytes, a stream or a 
file (`producerIn`). So the validator
+reports the `to`:
+
+[source,text]
+----
+to: file needs the body as text, bytes, a stream or a file, but the body here 
is parsed data (a Map or a List), not
+text - unmarshal: json turns the text into data: add marshal: json before this 
step
+----
+
+The source editor of the Camel CLI checks a file when you save it, and does 
not save a YAML file with a problem. The
+`to` line is marked, and the Error panel says what is wrong, with *Shift+F8* 
to fix it with AI (the notes on the right
+were added for this page):
+
+image::jbang/camel-tui-source-body-type-problem.png[The orders route in the 
source editor of the Camel CLI: the to line is marked, and the Error panel says 
that the file producer needs text, bytes, a stream or a file, but the body is 
parsed data after unmarshal json]
+
+With a `marshal` before it, the body is JSON text again, which the `file` 
producer writes:
+
+[source,yaml]
+----
+- route:
+    from:
+      uri: file:inbox
+      steps:
+        - unmarshal:
+            json: {}
+        - marshal:
+            json: {}
+        - to:
+            uri: file:outbox
+----
+
+=== Options change the body
+
+The body after a step can depend on its options. The `xslt` producer leaves 
the result as text, unless its `output`
+option says otherwise:
+
+[source,json]
+----
+"producerOut": {
+  "carriers": [ "text" ], "javaType": "java.lang.String", "dependsOn": [ 
"resultHandlerFactory" ],
+  "byOption": [
+    { "option": "output", "value": "bytes", "carriers": [ "bytes" ], 
"javaType": "byte[]" },
+    { "option": "output", "value": "DOM", "carriers": [ "pojo" ], "javaType": 
"org.w3c.dom.Document" },
+    { "option": "output", "value": "file", "carriers": [ "file" ], "javaType": 
"java.io.File" } ],
+  "description": "The result of the transformation" }
+----
+
+With `output=bytes` the body is a `byte[]`. With a `resultHandlerFactory` the 
factory decides the body, which the
+catalog cannot know, so it is not known.
+
+=== An option that names the class of the body
+
+Some options name the class the result is converted to: the `resultType` of a 
language, the `unmarshalType` of a JSON
+data format, the `type` of `convertBodyTo`, or the `outputClass` of `sql` for 
one row. When such an option is set, the
+body is an object of that class, or null:
+
+[source,yaml]
+----
+- route:
+    from:
+      uri: direct:order
+      steps:
+        - setBody:
+            simple:
+              expression: "${header.quantity}"
+              resultType: java.lang.Integer
+        - to:
+            uri: sql:select * from orders where id = :#id
+            parameters:
+              outputType: SelectOne
+              outputClass: com.acme.Order
+----
+
+`setBody` leaves the result of its expression, and with `resultType` that is 
an `Integer`, a `value`. The `sql`
+producer leaves a `List` of rows by default, and with `outputType=SelectOne` 
and `outputClass` one `com.acme.Order`,
+or null when there is no row.
+
+=== EIPs
+
+The body after an EIP is often the body of something else: `marshal` leaves 
what its data format writes, `to` what
+the producer of its endpoint leaves, and `setBody` the result of its 
expression. The catalog says so with `from`,
+instead of copying the body types of every data format, component and language:
+
+[source,json]
+----
+"eipOut": { "from": "marshalOut", "dependsOn": [ "variableReceive" ],
+  "description": "What the data format writes. With variableReceive the result 
goes into the variable, and the body stays as it was" }
+----
+
+An EIP with steps of its own also says what they start with. The steps of a 
`split` start with each part, and after
+it the body is as it was before the split:
+
+[source,json]
+----
+"eipOut": { "carriers": [ "unchanged" ], "dependsOn": [ "aggregationStrategy" 
],
+  "description": "The body as it was before the split, unless an 
aggregationStrategy is set" },
+"eipSteps": { "carriers": [ "unknown" ],
+  "description": "Each part of what the expression gives, such as an element 
of a List or a line of text" }
+----
+
+== Tooling
+
+=== Documentation
+
+The page of each component, data format, language and EIP has a _Message Body_ 
section with a row per side, which
+is generated from the catalog.
+
+=== Camel CLI and its MCP server
+
+`camel catalog doc` prints the same table for a component, data format, 
language or EIP, such as
+`camel catalog doc xslt` or `camel catalog doc eip:split`. The 
`camel_catalog_doc` tool of the
+xref:camel-jbang-mcp.adoc[MCP server] gives each side as a line of text, so an 
AI assistant reads them with the
+options:
+
+[source,text]
+----
+text (java.lang.String). The result of the transformation. With output=bytes: 
bytes (byte[]); output=DOM: pojo
+(org.w3c.dom.Document); output=file: file (java.io.File). Not known when 
resultHandlerFactory is set
+----
+
+=== YAML validator
+
+`camel validate source` follows the body through the steps of each route with 
the body types, and reports a step
+that cannot take the body it gets (see 
xref:camel-jbang-validation.adoc[Validation]). Where the catalog does not
+know the body, such as after an HTTP consumer, it says nothing, unless a 
running integration has shown what the body
+is.
+
+=== Asking an AI assistant
+
+An AI assistant with the xref:camel-jbang-mcp.adoc[MCP server] of the Camel 
CLI can answer "is something wrong with
+my route?". It validates the file with the `camel_validate_source` tool, which 
follows the body with the body types:
+
+[source,text]
+----
+You:        Is something wrong with my route in orders.camel.yaml? The files 
never show up in outbox.
+
+Assistant:  (calls camel_validate_source on orders.camel.yaml)
+            {"valid": false, "errors": ["Line 8: to: to: file needs the body 
as text, bytes, a stream or a file,
+             but the body here is parsed data (a Map or a List), not text - 
unmarshal: json turns the text into
+             data: add marshal: json before this step"]}
+
+            Yes. unmarshal: json turns each file into a Map (or a List for a 
JSON array), and the file producer
+            cannot write a Map, so the exchange fails at the to step and 
nothing reaches outbox. Add
+            marshal: json before the to to write the JSON again. If the route 
only moves the files, remove the
+            unmarshal instead, as the file consumer gives the file as it is.
+----
+
+The assistant can also read the body types of a step with `camel_catalog_doc`, 
such as what `unmarshal: json` leaves
+with `useList`, and it validates a route before it writes the file. In the 
source editor of the Camel CLI, *Shift+F8*
+asks the AI of the Camel CLI to fix the problem of a line.
+
+=== In a visual route editor
+
+As the body types are in the catalog, a visual route editor can show them too: 
the body on each arrow, and a warning
+on the step that cannot take it, with the step to add. An illustration of the 
route above:
+
+image::body-types-route-diagram.svg[An illustration of a route diagram: from 
file, unmarshal json, log and to file, with the body on each arrow, and a 
yellow warning triangle on the to step, whose callout says that the body is 
parsed data and offers to add marshal json before it]
+
+=== At runtime
+
+With the message history on, such as with `camel run --dev`, Camel records the 
types and kinds of the bodies each
+step has been reached with:
+
+* xref:components:eips:message-history.adoc[Message History] explains the 
kinds, the `BodyTypeRegistry`, the
+  `body-type` dev console and the JMX attributes.
+* `camel cmd route-structure` shows them per step (see 
xref:camel-jbang-diagnostics.adoc[Diagnostics]), as do the
+  xref:camel-jbang-tui-diagram.adoc#_body_types[diagram] and the
+  xref:camel-jbang-tui-source-editor.adoc#_body_types[source editor] of the 
Camel CLI.
+* The route coverage report of the xref:camel-report-maven-plugin.adoc[Camel 
Report Maven Plugin] shows the body types
+  the tests reached each step with, and can fail the build on a step reached 
with bodies of more than one kind.
+
+For example, the route coverage report of tests that sent two messages, where 
only one was parsed before the `to`:
+
+[source,bash]
+----
+mvn camel-report:route-coverage -Dcamel.bodyTypes=true
+----
+
+[source,text]
+----
+  Line #       Count    Route          Body types
+  ------       -----    -----          ----------
+       3           2    from
+       4           2      unmarshal    String
+       7           2      to           LinkedHashMap | String (data and 
payload)
+----
+
+The `to` has been reached with a `LinkedHashMap` (data) by one message and a 
`String` (a payload) by the other, so it
+works for some messages only. The body types in the catalog tell this before 
the route runs; the ones recorded at
+runtime show what the messages really brought.
+
+== Declaring body types
+
+A component declares its body types with the `@BodyTypes` annotation on its 
endpoint class, a data format and a
+language on their class, and an EIP on its model class in `camel-core-model`. 
That is also where the EIPs that other
+modules implement are declared, such as `circuitBreaker`. A subclass does not 
have the body types of its superclass,
+as it may change the body:
+
+[source,java]
+----
+@BodyTypes(consumerOut = @BodyType(value = Carrier.FILE, description = "The 
file as read ..."),
+           producerIn = @BodyType({ Carrier.TEXT, Carrier.BYTES, 
Carrier.STREAM, Carrier.FILE }),
+           producerOut = @BodyType(Carrier.UNCHANGED))
+@UriEndpoint(firstVersion = "1.0.0", scheme = "file", ...)
+public class FileEndpoint extends GenericFileEndpoint<File> {
+----
+
+The values come from the code that sets the body. The build checks that:
+
+* each side belongs to the kind of model; a consumer-only component has no 
producer sides;
+* the options a side names exist, and a value of an option is one the option 
can have;
+* `unchanged` and `unknown` are only on the body after a step, and `any` only 
on what a step needs;
+* `from` names a side the body can come from, and a side with `from` has no 
carriers of its own;
+* a `javaTypeOption` is an option of the body after a step that the side does 
not also depend on.
+
+The runtime body types the message history records are checked against them in 
tests, such as for the EIPs of core.
+
+== Reference: the JSON of the body types
+
+This section is for the tools, and the AI assistants, that read the catalog.
+
+The JSON of a component, data format, language or EIP in the catalog has a 
`bodyTypes` object next to its options, with
+an object per side. Each side has these fields; a field that does not apply is 
left out:
+
+[width="100%",cols="1,1,4",options="header"]
+|===
+| Field | Type | Meaning
+| `carriers` | array of strings | The carriers of the body: one, or several 
when the body can be any of them. Left out
+for a side with `from`.
+| `javaType` | string | The Java type of the body, when it is always of this 
type.
+| `format` | string | The format of the payload, when it is always the same: 
`json`, `xml`, `csv`, `yaml`, `text` or
+`binary`.
+| `dependsOn` | array of strings | The options the body depends on. The side 
holds while they keep their default values.
+When the step sets one, only a `byOption` entry for that value says the body; 
without one it is not known.
+| `byOption` | array of objects | The body for a value of an option: `option`, 
`value`, `carriers`, and optionally
+`javaType` and `javaTypeOption`.
+| `javaTypeOption` | string | The option that names the class of the body, 
such as `resultType`. When the step sets
+it, the body is an object of that class, or null.
+| `from` | string | For an EIP, the side of another model the body is: 
`expressionIn`, `expressionOut` (the language
+of its expression), `marshalIn`, `marshalOut`, `unmarshalIn`, `unmarshalOut` 
(its data format), `producerIn`,
+`producerOut` (its endpoint as a producer), `consumerOut` (its endpoint as a 
consumer), or `steps` (what its own
+steps end with).
+| `description` | string | What to know about the body, in a sentence.
+|===
+
+=== Reading a side for a step
+
+To know the body a step leaves, or the bodies it needs, read its side with the 
options the step sets, in this order:
+
+. *No side:* not known.
+. *A side with `from`:* when the step sets an option of `dependsOn`, not 
known. Otherwise, read the side `from` names on
+the model the step uses (the language of its expression, its data format or 
its endpoint), with the options of that
+model. `steps` is the body the steps of the EIP end with.
+. *An option the side names whose value is not known before the route runs* (a 
`+{{placeholder}}+` or a `+${expression}+`),
+or an option the step does not set but that can be set elsewhere, such as on 
the component: not known.
+. *An option of `dependsOn` the step sets:* the `byOption` entry for that 
value says the body. When it has a
+`javaTypeOption` the step sets, the body is an object of that class. Without 
such an entry: not known.
+. *The `javaTypeOption` of the side, when the step sets it:* an object of the 
class it names, or null. It wins over the
+`byOption` entries of other options.
+. *The `byOption` entries for the values the step sets:* their carriers (or 
the class of their `javaTypeOption`). When
+two of them say different bodies: not known.
+. *Otherwise:* the carriers of the side. `unknown` is not known, and 
`unchanged` is the body before the step.
+
+The values of an option compare in any case, and in any style of an enum 
constant, as Camel takes them:
+`convert-to-json`, `convertToJson` and `CONVERT_TO_JSON` are one value.
+
+The carrier of a class that a `javaTypeOption` names follows the class: 
`String` is `text`, `byte[]` is `bytes`, an
+`InputStream` or a `Reader` is `stream`, a `File` or a `Path` is `file`, a 
number, a boolean or a character is
+`value`, a `Map` is `map`, a `Collection` or an array is `list`, an `Iterator` 
is `iterator`, `Object` is `unknown`, and
+any other class is `pojo`. The short names Camel takes, such as `String` and 
`int`, count too.
+
+=== Java API
+
+The models of `camel-catalog` have the body types, and `BodyTypeResolver` of 
`camel-tooling-model` reads a side for the
+options of a step, by the rule above. The YAML validator uses it:
+
+[source,java]
+----
+CamelCatalog catalog = new DefaultCamelCatalog();
+
+BodyTypeModel out = 
catalog.componentModel("xslt").getBodyType(BodyTypeModel.PRODUCER_OUT);
+out.getCarriers();      // [text]
+out.getJavaType();      // java.lang.String
+
+BodyTypeResolver.Resolved bytes = BodyTypeResolver.resolve(out, 
Map.of("output", "bytes"), Set.of());
+bytes.carriers();       // [bytes]
+
+BodyTypeModel.carrierOf("java.util.Map");   // map
+----
diff --git a/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc
index 2e7cc5513d6b..864526f0844e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc
@@ -129,86 +129,20 @@ question people, IDEs and AI agents ask most when they 
write the next step of a
 file it read, the `http` producer leaves the response as a byte array or a 
stream, and after `unmarshal: json` the body
 is a `Map` or a `List`.
 
-A component, data format, language or EIP can declare its body types, and the 
catalog then has a `bodyTypes` section
-with one entry per side:
-
-[width="100%",cols="25%,75%",options="header",]
-|===
-|Side |The body
-|`consumerOut` |The body of the message the consumer creates.
-|`consumerReply` |The bodies a consumer that answers the caller can reply 
with: the body the route ends with.
-|`producerIn` |The bodies the producer can send or write.
-|`producerOut` |The body after the producer.
-|`marshalIn`, `marshalOut` |The bodies a data format can marshal, and the body 
after marshal.
-|`unmarshalIn`, `unmarshalOut` |The bodies a data format can unmarshal, and 
the body after unmarshal.
-|`expressionIn`, `expressionOut` |The bodies the expressions of a language can 
read, and their result.
-|`eipIn` |The bodies an EIP needs, such as text for `tokenizer`.
-|`eipOut` |The body after an EIP.
-|`eipSteps` |The body the steps of an EIP start with, such as each part for 
`split`.
-|===
-
-Each side has the _carriers_ of the body, the kinds of Java object it is in: 
`null`, `text`, `bytes`, `stream`,
-`file`, `value`, `map`, `list`, `iterator` (read once, with no size and no 
index) and `pojo`; `unchanged` for a step
-that leaves the body as it was, `any` for a step that takes any body, and 
`unknown` for a body that depends on what
-the other system sends or on a class the user configures. A side can also have 
the Java type when the body is always of that type, the format of the payload 
(json,
-xml, csv, yaml, text or binary), the options the body depends on, the body for 
other values of an option, and a
-description:
+A component, data format, language or EIP that declares its body types has a 
`bodyTypes` section in the catalog, with
+an entry per side: what a consumer gives, what a producer needs and leaves, 
what a data format marshals and unmarshals
+to, what an expression reads and gives, and what an EIP needs, leaves and 
starts its own steps with:
 
 [source,json]
 ----
 "bodyTypes": {
-  "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "pojo" ], 
"format": "xml", "dependsOn": [ "source" ],
-    "description": "The XML to transform: text, bytes, a stream, a file, a DOM 
or another javax.xml.transform.Source. ..." },
-  "producerOut": { "carriers": [ "text" ], "javaType": "java.lang.String", 
"dependsOn": [ "resultHandlerFactory" ],
-    "byOption": [ { "option": "output", "value": "bytes", "carriers": [ 
"bytes" ], "javaType": "byte[]" }, ... ],
-    "description": "The result of the transformation" }
+  "producerIn": { "carriers": [ "text", "bytes", "stream", "file" ],
+    "description": "What converts to an InputStream: text, bytes, a stream or 
a file. A Map or a List fails: marshal it first" },
+  "producerOut": { "carriers": [ "unchanged" ] }
 }
 ----
 
-An EIP whose body is the body of another model says which side of it with 
`from`, instead of carriers of its own:
-`marshal` leaves what its data format leaves (`marshalOut`), `to` what the 
producer of its endpoint leaves
-(`producerOut`), `pollEnrich` what the consumer of its endpoint receives 
(`consumerOut`), `setBody` the result of its
-expression (`expressionOut`), and a `filter` what its own steps end with 
(`steps`):
-
-[source,json]
-----
-"bodyTypes": {
-  "eipOut": { "from": "steps", "description": "What its steps end with; the 
body as it was when the predicate does not match" },
-  "eipSteps": { "carriers": [ "unchanged" ] }
-}
-----
-
-An option that names the class of the body is the `javaTypeOption` of a side, 
such as the `resultType` of a language,
-whose result is converted to that class, the `unmarshalType` of a JSON data 
format, and the `type` of `convertBodyTo`.
-When the option is set, the body is an object of the class it names, or null 
when it does not convert. That holds while
-the options the side depends on keep their defaults (an option that makes the 
body a collection of that class, such
-as `useList` of `jackson`, is one of them), and it wins over the bodies 
`byOption` gives for other options. The
-carrier follows the class: `String` is `text`, `byte[]` is `bytes`, an 
`InputStream` or a `Reader` is a `stream`, a
-`File` or a `Path` is a `file`, a number, a boolean or a character is a 
`value`, a `Map` is a `map`, a `Collection` or
-an array is a `list`, an `Iterator` is an `iterator`, `Object` is `unknown`, 
and any other class is a `pojo`.
-`BodyTypeModel.carrierOf` gives it for a class name, also for the short names 
Camel takes, such as `String` and `int`:
-
-[source,json]
-----
-"bodyTypes": {
-  "expressionOut": { "carriers": [ "unknown" ], "javaTypeOption": "resultType",
-    "description": "A text with functions gives text. A single function, such 
as ${body} or ${header.foo}, gives its value as it is" }
-}
-----
-
-A body for an option value can have a `javaTypeOption` too, when the option 
names the class of the body only for that
-value: the `outputClass` of `sql` is the class of the one row with 
`outputType=SelectOne`, and of each row in the List
-otherwise:
-
-[source,json]
-----
-"producerOut": { "carriers": [ "list", "unchanged" ], "dependsOn": [ 
"outputHeader", "noop" ],
-  "byOption": [ { "option": "outputType", "value": "SelectOne", "carriers": [ 
"map", "value", "null" ], "javaTypeOption": "outputClass" }, ... ] }
-----
-
-The sides say what the body is with the default options. A side that is not in 
the catalog is not known, and tooling
-should not guess it. The documentation page of a component, data format, 
language or EIP shows them in its _Message
-Body_ section, and `camel catalog doc` prints them. The Java API gives them on 
the model:
+The Java API gives them on the model:
 
 [source,java]
 ----
@@ -219,30 +153,8 @@ out.getCarriers();     // [text]
 out.getJavaType();     // java.lang.String
 ----
 
-Component developers declare them with the `@BodyTypes` annotation on the 
endpoint, data format or language class, and
-the EIPs on their model class in `camel-core-model`, also when the EIP is 
implemented in another module, such as
-`circuitBreaker`. A subclass does not have the body types of its superclass, 
as it may change the body:
-
-[source,java]
-----
-@BodyTypes(consumerOut = @BodyType(value = Carrier.FILE, description = "The 
file as read ..."),
-           producerIn = @BodyType({ Carrier.TEXT, Carrier.BYTES, 
Carrier.STREAM, Carrier.FILE }),
-           producerOut = @BodyType(Carrier.UNCHANGED))
-@UriEndpoint(firstVersion = "1.0.0", scheme = "file", ...)
-public class FileEndpoint extends GenericFileEndpoint<File> {
-----
-
-The build checks that each side belongs to the kind (a consumer-only component 
has no producer sides), that the
-options a side names exist, that a value of an option is one the option can 
have (such as `chat-completion` for the
-`operation` of openai), that `unchanged` is only on the body after a step and 
`any` only on what a step needs,
-that `from` names a side the body can come from, and that a `javaTypeOption` 
is an option of the body after a step
-that the side does not also depend on.
-The values are read from the code that sets the body, and checked against the 
body types the message history records
-when the routes run.
-
-The YAML validator of the Camel CLI follows the body through the steps of a 
route with the body types, and reports a
-step that cannot work on it, such as a Map that a `to: file` cannot write (see
-xref:camel-jbang-validation.adoc#_what_a_running_integration_saw[Camel CLI 
validation]).
+See xref:body-types.adoc[Message Body Types] for what the body types are, the 
tools that use them, how components
+declare them, and the JSON of each side with the rule to read it for the 
options of a step.
 
 === Documentation in the Catalog
 
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 305f64e8ccd0..15dae476ac2a 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -318,7 +318,7 @@ project `directory` as an argument, the runtime tools take 
the integration `name
   its group of `application.properties` keys, the key first. A component, data 
format or language that
   declares its body types has them in `bodyTypes`, a line per side: what a 
`from:` gives the route
   (`consumerOut`), what a `to:` can send (`producerIn`) and the body after it 
(`producerOut`), and so on (see
-  xref:camel-catalog.adoc#_body_types_in_the_catalog[Body types in the 
Catalog]). An answer is cut at 18 000
+  xref:body-types.adoc[Message Body Types]). An answer is cut at 18 000
   characters, with a marker that says what was left out.
   `includeOptions` picks the options listed: `common` (the default) leaves out 
the deprecated and advanced
   ones and says how many with `omittedOptions`, `required` lists the required 
ones, `all` everything, `false`
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc
index 54c39089b805..709da62cf506 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc
@@ -76,7 +76,7 @@ A part of a Java route the parser cannot work out (a value 
computed at runtime)
 The YAML validator follows the message body through the steps of each route, 
and reports a step that cannot work on
 it: a field read such as `+${body.qty}+` on text that was never unmarshalled, 
or a Map that a `to: file` cannot write.
 What a component or a data format does with the body comes from the
-xref:camel-catalog.adoc#_body_types_in_the_catalog[body types in the catalog]: 
the file consumer gives the file it
+xref:body-types.adoc[body types in the catalog]: the file consumer gives the 
file it
 read, the `http` producer leaves the response as bytes or a stream, and 
`unmarshal: json` makes a Map or a List. The
 answer of `openai:chat-completion` is text, also when it is asked for JSON 
with `jsonSchema`, so a field read on it
 needs `unmarshal: json` first. Where they do not tell what the body is, the 
validator says nothing: after a `platform-http` consumer the body
diff --git a/docs/user-manual/modules/ROOT/pages/endpoint-annotations.adoc 
b/docs/user-manual/modules/ROOT/pages/endpoint-annotations.adoc
index aa9b755a18b8..6bee27e2e912 100644
--- a/docs/user-manual/modules/ROOT/pages/endpoint-annotations.adoc
+++ b/docs/user-manual/modules/ROOT/pages/endpoint-annotations.adoc
@@ -33,8 +33,8 @@ annotated with one or more `@UriParam` or `@UriParams` 
annotations
 |`@Metadata` | Used for special situations to provide additional information.
 
 |`@BodyTypes` | The message body the endpoint works with: the body the 
consumer creates, the bodies the
-producer can send, and the body after the producer. See 
xref:camel-catalog.adoc#_body_types_in_the_catalog[Body types
-in the Catalog].
+producer can send, and the body after the producer. See 
xref:body-types.adoc[Message
+Body Types].
 |=======================================================================
 
 For example see the `TimerEndpoint` from the `camel-timer` component and
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/CatalogBodyTypes.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/CatalogBodyTypes.java
index a2ff7f8b230c..6cd0a7753bf4 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/CatalogBodyTypes.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/CatalogBodyTypes.java
@@ -20,7 +20,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -31,11 +30,10 @@ import com.fasterxml.jackson.databind.JsonNode;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.tooling.model.BaseOptionModel;
 import org.apache.camel.tooling.model.BodyTypeModel;
-import org.apache.camel.tooling.model.BodyTypeModel.OptionBodyTypeModel;
+import org.apache.camel.tooling.model.BodyTypeResolver;
 import org.apache.camel.tooling.model.ComponentModel;
 import org.apache.camel.tooling.model.DataFormatModel;
 import org.apache.camel.tooling.model.EipModel;
-import org.apache.camel.util.StringHelper;
 
 import static org.apache.camel.dsl.yaml.validator.RouteGraph.scheme;
 
@@ -234,85 +232,12 @@ final class CatalogBodyTypes {
     }
 
     /**
-     * What a side says for the options a step sets: the body for an option 
value it names, or its body when the options
-     * it depends on keep their defaults; null when it is not known for the 
step. An option the side depends on decides
-     * first; then the option that names the class of the body (CAMEL-25928), 
and then the bodies for the values of the
-     * other options.
+     * What a side says for the options a step sets, by the rule the tools 
share (BodyTypeResolver of the tooling
+     * model); null when it is not known for the step.
      */
     private static Answer answer(BodyTypeModel side, Map<String, String> set, 
Set<String> configurable) {
-        if (side == null) {
-            return null;
-        }
-        Set<String> options = new LinkedHashSet<>(side.getDependsOn());
-        side.getByOption().forEach(o -> options.add(o.getOption()));
-        if (side.getJavaTypeOption() != null) {
-            options.add(side.getJavaTypeOption());
-        }
-        
side.getByOption().stream().map(OptionBodyTypeModel::getJavaTypeOption).filter(o
 -> o != null)
-                .forEach(options::add);
-        for (String option : options) {
-            String value = set.get(option);
-            if (value == null ? configurable.contains(option) : 
value.contains("{{") || value.contains("${")) {
-                // the component may set it, or its value is only known when 
the route runs
-                return null;
-            }
-        }
-        for (String option : side.getDependsOn()) {
-            String value = set.get(option);
-            if (value != null && side.getByOption().stream()
-                    .noneMatch(w -> w.getOption().equals(option) && 
isSameValue(value, w.getValue()))) {
-                // the body depends on this option, and no option value of the 
side says the body for its value: the
-                // value another option names does not hold then, such as 
output=bytes with a resultHandlerFactory
-                return null;
-            }
-        }
-        boolean dependsOnSet = 
side.getDependsOn().stream().anyMatch(set::containsKey);
-        if (!dependsOnSet && side.getJavaTypeOption() != null && 
set.containsKey(side.getJavaTypeOption())) {
-            // the option names the class of the body, whatever the values of 
the other options say
-            return ofClass(set.get(side.getJavaTypeOption()));
-        }
-        Answer matched = null;
-        for (OptionBodyTypeModel when : side.getByOption()) {
-            String value = set.get(when.getOption());
-            if (value != null && isSameValue(value, when.getValue())) {
-                Answer answer = when.getJavaTypeOption() != null && 
set.containsKey(when.getJavaTypeOption())
-                        ? ofClass(set.get(when.getJavaTypeOption()))
-                        : new Answer(when.getCarriers(), when.getJavaType(), 
side.getFormat());
-                if (answer == null || matched != null && 
!matched.carriers().equals(answer.carriers())) {
-                    // two options the step sets say different bodies
-                    return null;
-                }
-                matched = answer;
-            }
-        }
-        if (matched != null) {
-            return matched;
-        }
-        if (side.isUnknown() || 
side.getDependsOn().stream().anyMatch(set::containsKey)) {
-            return null;
-        }
-        return new Answer(side.getCarriers(), side.getJavaType(), 
side.getFormat());
-    }
-
-    /**
-     * The body of the class an option names, such as unmarshalType: an object 
of it, or null when it does not convert.
-     * Object says nothing.
-     */
-    private static Answer ofClass(String javaType) {
-        String carrier = BodyTypeModel.carrierOf(javaType);
-        if ("unknown".equals(carrier)) {
-            return null;
-        }
-        return new Answer(List.of(carrier, "null"), javaType.trim(), null);
-    }
-
-    /**
-     * Whether the value a step sets is the value of an option body: in any 
case, and in any style of an enum constant,
-     * as the catalog and the enum type converter take it, so 
operation=convert-to-json is CONVERT_TO_JSON.
-     */
-    private static boolean isSameValue(String value, String other) {
-        return value.equalsIgnoreCase(other)
-                || 
StringHelper.asEnumConstantValue(value).equals(StringHelper.asEnumConstantValue(other));
+        BodyTypeResolver.Resolved resolved = BodyTypeResolver.resolve(side, 
set, configurable);
+        return resolved != null ? new Answer(resolved.carriers(), 
resolved.javaType(), resolved.format()) : null;
     }
 
     /** The options of a node, by name in camel case: allowNullBody as well 
for allow-null-body. */
diff --git 
a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/BodyTypeResolver.java
 
b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/BodyTypeResolver.java
new file mode 100644
index 000000000000..838e12c93206
--- /dev/null
+++ 
b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/BodyTypeResolver.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.tooling.model;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.camel.tooling.model.BodyTypeModel.OptionBodyTypeModel;
+
+/**
+ * What one side of the body types of the catalog says for the options a step 
sets (CAMEL-24961, CAMEL-25928): the rule
+ * that the YAML validator and the other tools share, so they all read the 
catalog the same way.
+ * <p/>
+ * The rule, in order:
+ * <ol>
+ * <li>An option the side names (in dependsOn, byOption or javaTypeOption) 
whose value is only known when the route
+ * runs, a {@code {{placeholder}}} or a {@code ${expression}}, or that the 
step does not set but can be set elsewhere,
+ * such as on the component: not known.</li>
+ * <li>An option of dependsOn the step sets: the body of byOption for that 
value decides, and without one it is not
+ * known.</li>
+ * <li>Otherwise, the javaTypeOption the step sets: an object of the class it 
names, or null.</li>
+ * <li>Otherwise, the bodies of byOption for the values the step sets; when 
two say different bodies, not known.</li>
+ * <li>Otherwise, the carriers of the side; unknown is not known.</li>
+ * </ol>
+ * A body of byOption that has a javaTypeOption the step sets is an object of 
that class, or null. The values of an
+ * option compare in any case, and in any style of an enum constant, as Camel 
takes them: convert-to-json is
+ * CONVERT_TO_JSON. A side with from is the body of a side of another model, 
which the caller resolves.
+ */
+public final class BodyTypeResolver {
+
+    private BodyTypeResolver() {
+    }
+
+    /**
+     * What a side says for a step: the carriers of the body, and its Java 
type and format when it has them.
+     */
+    public record Resolved(List<String> carriers, String javaType, String 
format) {
+
+        public Resolved {
+            // a copy, not the list of the model the caller could change
+            carriers = List.copyOf(carriers);
+        }
+    }
+
+    /**
+     * What a side says for the options a step sets.
+     *
+     * @param  side    the side, null when the catalog has none
+     * @param  options the options the step sets, by name: the endpoint uri, 
the parameters or the options of a data
+     *                 format
+     * @param  hidden  the options that can be set elsewhere when the step 
does not set them, such as the options of the
+     *                 component
+     * @return         the body, or null when it is not known for the step
+     */
+    public static Resolved resolve(BodyTypeModel side, Map<String, String> 
options, Set<String> hidden) {
+        if (side == null || side.getFrom() != null) {
+            return null;
+        }
+        Set<String> names = new LinkedHashSet<>(side.getDependsOn());
+        side.getByOption().forEach(o -> names.add(o.getOption()));
+        if (side.getJavaTypeOption() != null) {
+            names.add(side.getJavaTypeOption());
+        }
+        
side.getByOption().stream().map(OptionBodyTypeModel::getJavaTypeOption).filter(Objects::nonNull)
+                .forEach(names::add);
+        for (String name : names) {
+            String value = options.get(name);
+            if (value == null ? hidden.contains(name) : value.contains("{{") 
|| value.contains("${")) {
+                // it may be set elsewhere, or its value is only known when 
the route runs
+                return null;
+            }
+        }
+        boolean dependsOnSet = false;
+        for (String name : side.getDependsOn()) {
+            String value = options.get(name);
+            if (value != null) {
+                dependsOnSet = true;
+                if (side.getByOption().stream()
+                        .noneMatch(w -> w.getOption().equals(name) && 
isSameValue(value, w.getValue()))) {
+                    // the body depends on this option, and no option value of 
the side says the body for its value
+                    return null;
+                }
+            }
+        }
+        if (!dependsOnSet && side.getJavaTypeOption() != null && 
options.containsKey(side.getJavaTypeOption())) {
+            return ofClass(options.get(side.getJavaTypeOption()));
+        }
+        Resolved matched = null;
+        for (OptionBodyTypeModel when : side.getByOption()) {
+            String value = options.get(when.getOption());
+            if (value != null && isSameValue(value, when.getValue())) {
+                Resolved resolved = when.getJavaTypeOption() != null && 
options.containsKey(when.getJavaTypeOption())
+                        ? ofClass(options.get(when.getJavaTypeOption()))
+                        : new Resolved(when.getCarriers(), when.getJavaType(), 
side.getFormat());
+                if (resolved == null || matched != null && 
!matched.carriers().equals(resolved.carriers())) {
+                    // two options the step sets say different bodies
+                    return null;
+                }
+                matched = resolved;
+            }
+        }
+        if (matched != null) {
+            return matched;
+        }
+        if (dependsOnSet || side.isUnknown()) {
+            return null;
+        }
+        return new Resolved(side.getCarriers(), side.getJavaType(), 
side.getFormat());
+    }
+
+    /**
+     * The body of the class an option names: an object of it, or null when it 
does not convert. Object says nothing.
+     */
+    private static Resolved ofClass(String javaType) {
+        String carrier = BodyTypeModel.carrierOf(javaType);
+        if ("unknown".equals(carrier)) {
+            return null;
+        }
+        return new Resolved(List.of(carrier, "null"), javaType.trim(), null);
+    }
+
+    /**
+     * Whether a value of an option is the value a body is for: in any case, 
and in any style of an enum constant, as
+     * the catalog and the enum type converter take it, so convert-to-json and 
convertToJson are CONVERT_TO_JSON.
+     */
+    public static boolean isSameValue(String value, String other) {
+        return value.equalsIgnoreCase(other) || 
asEnumConstant(value).equals(asEnumConstant(other));
+    }
+
+    /** The value as an enum constant, as StringHelper.asEnumConstantValue in 
camel-util gives it. */
+    private static String asEnumConstant(String value) {
+        return camelCaseToDash(value).replaceAll("-+", "-").replace('-', 
'_').toUpperCase(Locale.ENGLISH);
+    }
+
+    /** As StringHelper.camelCaseToDash in camel-util, which this module does 
not depend on. */
+    private static String camelCaseToDash(String text) {
+        StringBuilder answer = new StringBuilder(text.length() + 8);
+        char prev = 0;
+        char[] arr = text.toCharArray();
+        for (int i = 0; i < arr.length; i++) {
+            char ch = arr[i];
+            if (ch == '-' || ch == '_') {
+                answer.append('-');
+            } else if (Character.isUpperCase(ch) && prev != 0) {
+                char next = i < arr.length - 1 ? arr[i + 1] : 0;
+                if (!Character.isUpperCase(prev) || next != 0 && 
Character.isLowerCase(next)) {
+                    if (prev != '-' && prev != '_') {
+                        answer.append('-');
+                    }
+                }
+                answer.append(Character.toLowerCase(ch));
+            } else {
+                answer.append(Character.toLowerCase(ch));
+            }
+            prev = ch;
+        }
+        return answer.toString();
+    }
+}
diff --git 
a/tooling/camel-tooling-model/src/test/java/org/apache/camel/tooling/model/BodyTypeResolverTest.java
 
b/tooling/camel-tooling-model/src/test/java/org/apache/camel/tooling/model/BodyTypeResolverTest.java
new file mode 100644
index 000000000000..bc9f75acbea4
--- /dev/null
+++ 
b/tooling/camel-tooling-model/src/test/java/org/apache/camel/tooling/model/BodyTypeResolverTest.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.tooling.model;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.tooling.model.BodyTypeModel.OptionBodyTypeModel;
+import org.apache.camel.tooling.model.BodyTypeResolver.Resolved;
+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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The rule the tools share to read a side of the body types for the options a 
step sets (CAMEL-25928).
+ */
+class BodyTypeResolverTest {
+
+    @Test
+    void theCarriersWithTheDefaultOptions() {
+        BodyTypeModel side = side(List.of("map", "list"));
+        side.setFormat("json");
+        assertEquals(new Resolved(List.of("map", "list"), null, "json"), 
resolve(side, Map.of()));
+        assertNull(resolve(side(List.of("unknown")), Map.of()));
+        assertNull(resolve(null, Map.of()));
+    }
+
+    @Test
+    void aSideFromAnotherModelIsResolvedByTheCaller() {
+        BodyTypeModel side = new BodyTypeModel();
+        side.setFrom(BodyTypeModel.MARSHAL_OUT);
+        assertNull(resolve(side, Map.of()));
+    }
+
+    @Test
+    void aValueKnownOnlyWhenTheRouteRunsIsNotKnown() {
+        BodyTypeModel side = jackson();
+        assertNull(resolve(side, Map.of("unmarshalType", "{{type}}")));
+        assertNull(resolve(side, Map.of("useList", "${header.list}")));
+        // an option the component can set, and the step does not
+        assertNull(BodyTypeResolver.resolve(side, Map.of(), 
Set.of("useList")));
+    }
+
+    @Test
+    void anOptionTheBodyDependsOnDecidesFirst() {
+        BodyTypeModel side = jackson();
+        assertEquals(List.of("list"), resolve(side, Map.of("useList", "true", 
"unmarshalType", "com.acme.Order")).carriers());
+        // no body for its value: not known, also with the class
+        assertNull(resolve(side, Map.of("collectionType", "java.util.Set", 
"unmarshalType", "com.acme.Order")));
+        assertNull(resolve(side, Map.of("useList", "false")));
+    }
+
+    @Test
+    void theOptionThatNamesTheClassOfTheBody() {
+        BodyTypeModel side = jackson();
+        Resolved order = resolve(side, Map.of("unmarshalType", 
"com.acme.Order"));
+        assertEquals(new Resolved(List.of("pojo", "null"), "com.acme.Order", 
null), order);
+        assertEquals(List.of("text", "null"), resolve(side, 
Map.of("unmarshalType", "String")).carriers());
+        // Object can be anything
+        assertNull(resolve(side, Map.of("unmarshalType", "java.lang.Object")));
+    }
+
+    @Test
+    void theOptionThatNamesTheClassWinsOverTheValuesOfOtherOptions() {
+        BodyTypeModel side = side(List.of("pojo"));
+        side.setJavaType("org.w3c.dom.NodeList");
+        side.setJavaTypeOption("resultType");
+        side.getByOption().add(when("resultQName", "STRING", List.of("text"), 
null));
+        assertEquals(List.of("text"), resolve(side, Map.of("resultQName", 
"string")).carriers());
+        assertEquals(List.of("value", "null"),
+                resolve(side, Map.of("resultQName", "STRING", "resultType", 
"java.lang.Integer")).carriers());
+    }
+
+    @Test
+    void anOptionValueCanNameTheClassToo() {
+        BodyTypeModel side = side(List.of("list", "unchanged"));
+        side.getByOption().add(when("outputType", "SelectOne", List.of("map", 
"value", "null"), "outputClass"));
+        side.getByOption().add(when("outputType", "StreamList", 
List.of("iterator"), null));
+        assertEquals(List.of("map", "value", "null"), resolve(side, 
Map.of("outputType", "SelectOne")).carriers());
+        assertEquals(new Resolved(List.of("pojo", "null"), "com.acme.Order", 
null),
+                resolve(side, Map.of("outputType", "SelectOne", "outputClass", 
"com.acme.Order")));
+        // Object can be anything, also for an option value
+        assertNull(resolve(side, Map.of("outputType", "SelectOne", 
"outputClass", "java.lang.Object")));
+        // the class does not change the List of the default outputType
+        assertEquals(List.of("list", "unchanged"), resolve(side, 
Map.of("outputClass", "com.acme.Order")).carriers());
+        assertEquals(List.of("iterator"), resolve(side, Map.of("outputType", 
"stream-list")).carriers());
+    }
+
+    @Test
+    void theCarriersAreACopy() {
+        BodyTypeModel side = side(new ArrayList<>(List.of("text")));
+        Resolved resolved = resolve(side, Map.of());
+        side.getCarriers().add("bytes");
+        assertEquals(List.of("text"), resolved.carriers());
+    }
+
+    @Test
+    void twoOptionValuesThatSayDifferentBodiesAreNotKnown() {
+        BodyTypeModel side = side(List.of("text"));
+        side.getByOption().add(when("output", "bytes", List.of("bytes"), 
null));
+        side.getByOption().add(when("binary", "true", List.of("stream"), 
null));
+        assertNull(resolve(side, Map.of("output", "bytes", "binary", "true")));
+        assertEquals(List.of("bytes"), resolve(side, Map.of("output", 
"BYTES")).carriers());
+    }
+
+    @Test
+    void theValuesOfAnEnumInAnyStyle() {
+        assertTrue(BodyTypeResolver.isSameValue("convert-to-json", 
"CONVERT_TO_JSON"));
+        assertTrue(BodyTypeResolver.isSameValue("convertToJson", 
"CONVERT_TO_JSON"));
+        assertTrue(BodyTypeResolver.isSameValue("selectOne", "SelectOne"));
+        assertFalse(BodyTypeResolver.isSameValue("SelectList", "SelectOne"));
+    }
+
+    private static Resolved resolve(BodyTypeModel side, Map<String, String> 
options) {
+        return BodyTypeResolver.resolve(side, options, Set.of());
+    }
+
+    /** The body after unmarshal of jackson: a Map or a List, a List with 
useList, or the class of unmarshalType. */
+    private static BodyTypeModel jackson() {
+        BodyTypeModel side = side(List.of("map", "list"));
+        side.setDependsOn(List.of("collectionType", "useList", 
"allowUnmarshallType"));
+        side.setJavaTypeOption("unmarshalType");
+        side.getByOption().add(when("useList", "true", List.of("list"), null));
+        return side;
+    }
+
+    private static BodyTypeModel side(List<String> carriers) {
+        BodyTypeModel side = new BodyTypeModel();
+        side.setCarriers(carriers);
+        return side;
+    }
+
+    private static OptionBodyTypeModel when(String option, String value, 
List<String> carriers, String javaTypeOption) {
+        OptionBodyTypeModel when = new OptionBodyTypeModel();
+        when.setOption(option);
+        when.setValue(value);
+        when.setCarriers(carriers);
+        when.setJavaTypeOption(javaTypeOption);
+        return when;
+    }
+}
diff --git 
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/BodyTypesHelper.java
 
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/BodyTypesHelper.java
index 6369a6fa702f..06ab8560de80 100644
--- 
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/BodyTypesHelper.java
+++ 
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/BodyTypesHelper.java
@@ -21,7 +21,6 @@ import java.util.Arrays;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
@@ -32,8 +31,8 @@ import org.apache.camel.spi.BodyTypes;
 import org.apache.camel.tooling.model.BaseModel;
 import org.apache.camel.tooling.model.BaseOptionModel;
 import org.apache.camel.tooling.model.BodyTypeModel;
+import org.apache.camel.tooling.model.BodyTypeResolver;
 import org.apache.camel.tooling.model.ComponentModel;
-import org.apache.camel.tooling.util.Strings;
 
 /**
  * Carries the {@link BodyTypes} of a component, data format, language or EIP 
into its model, which the catalog JSON is
@@ -238,26 +237,13 @@ final class BodyTypesHelper {
         List<String> values = option.getEnums() != null && 
!option.getEnums().isEmpty()
                 ? option.getEnums()
                 : "boolean".equals(option.getType()) ? List.of("true", 
"false") : List.of();
-        if (!values.isEmpty() && values.stream().noneMatch(v -> isSameValue(v, 
when.value()))) {
+        if (!values.isEmpty() && values.stream().noneMatch(v -> 
BodyTypeResolver.isSameValue(v, when.value()))) {
             throw new IllegalArgumentException(
                     owner.getName() + ": the @BodyTypes " + side + " has " + 
when.option() + "=" + when.value()
                                                + ", which is not a value of 
the option: " + String.join(", ", values));
         }
     }
 
-    /**
-     * Whether two values of an option are one value as Camel takes it: in any 
case, and in any style of an enum
-     * constant, so convert-to-json and convertToJson are CONVERT_TO_JSON, as 
for the catalog and the enum type
-     * converter (StringHelper.asEnumConstantValue in camel-util, which this 
plugin does not depend on).
-     */
-    private static boolean isSameValue(String value, String other) {
-        return value.equalsIgnoreCase(other) || 
asEnumConstant(value).equals(asEnumConstant(other));
-    }
-
-    private static String asEnumConstant(String value) {
-        return Strings.camelCaseToDash(value).replaceAll("-+", 
"-").replace('-', '_').toUpperCase(Locale.ENGLISH);
-    }
-
     /**
      * A body has a carrier: an option value without one would say nothing. 
UNCHANGED is for the body after a step, such
      * as a sql query that leaves it unchanged unless it is a select. ANY is 
for the bodies a step needs, and UNKNOWN

Reply via email to