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 fd00fc52ab9e CAMEL-25928: camel-jbang - the body of a Kamelet from its 
data types (#28450)
fd00fc52ab9e is described below

commit fd00fc52ab9ea1f702119060ddc2a5c8c76a6436
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 25 15:25:26 2026 +0100

    CAMEL-25928: camel-jbang - the body of a Kamelet from its data types 
(#28450)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../user-manual/modules/ROOT/pages/body-types.adoc |  22 ++++
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |   3 +-
 .../dsl/jbang/core/commands/ai/CatalogDocs.java    |  60 ++++++++-
 .../jbang/core/commands/ai/KameletDefinitions.java |  63 +++++++++-
 .../dsl/jbang/core/commands/ai/ValidatorFacts.java |  39 +++++-
 .../jbang/core/commands/catalog/CatalogDoc.java    |   2 +-
 .../commands/catalog/KameletCatalogHelper.java     |  41 +++++++
 ...KameletModel.java => KameletDataTypeModel.java} |  20 +--
 .../jbang/core/commands/catalog/KameletModel.java  |   3 +
 .../core/commands/ai/KameletDataTypesTest.java     | 134 +++++++++++++++++++++
 .../dsl/jbang/core/commands/mcp/KameletTools.java  |  37 +++++-
 .../jbang/core/commands/mcp/KameletToolsTest.java  |  17 +++
 .../camel/dsl/yaml/validator/CatalogBodyTypes.java |  21 +++-
 .../camel/dsl/yaml/validator/ProjectFacts.java     |  20 ++-
 .../camel/dsl/yaml/validator/TextBodyFlow.java     |  81 ++++++++++++-
 .../camel/dsl/yaml/validator/TextBodyFlowTest.java |  74 ++++++++++++
 16 files changed, 611 insertions(+), 26 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/body-types.adoc 
b/docs/user-manual/modules/ROOT/pages/body-types.adoc
index 4daf9d2e38e1..53b77f8c6824 100644
--- a/docs/user-manual/modules/ROOT/pages/body-types.adoc
+++ b/docs/user-manual/modules/ROOT/pages/body-types.adoc
@@ -240,6 +240,28 @@ writes any object as JSON bytes, and the CloudEvent 
transformers of the componen
 `transformDataType` takes its body from them, by the name of its `toType`: 
`aws2-s3:application-cloudevents` is the
 transformer `aws2-s3-application-cloudevents` of the catalog.
 
+=== Kamelets
+
+A Kamelet says the body it takes and gives with its data types 
(`spec.dataTypes`), whose `format` names a data type
+transformer. Camel runs the transformer of the default `in` data type at the 
start of the Kamelet route, and of the
+default `out` one at its end, so the body of a Kamelet is what that 
transformer takes and leaves:
+
+[source,yaml]
+----
+spec:
+  dataTypes:
+    out:
+      default: binary
+      types:
+        binary:
+          format: application-octet-stream
+----
+
+The Camel CLI reads the data types of the Kamelets of the Kamelet catalog and 
of the project's own `.kamelet.yaml`
+files. The YAML validator walks a route that starts with `from: 
kamelet:aws-s3-source` with bytes. It reports a body
+that a Kamelet sink cannot take before a `to: kamelet:...`. 
`camel_catalog_kamelet_doc` lists the data types of a
+Kamelet with the body of each. A Kamelet without data types says nothing about 
its body.
+
 == Tooling
 
 === Documentation
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 15dae476ac2a..a90afeabb2b0 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -536,7 +536,8 @@ client to the next.
 
 | `camel_catalog_kamelet_doc`
 | Get detailed documentation for a specific Kamelet including all 
properties/options, their types, defaults,
-  examples, and the Kamelet's Maven dependencies.
+  examples, and the Kamelet's Maven dependencies. For a Kamelet with data 
types, each data type with its data type
+  transformer and the body it takes or gives (see xref:body-types.adoc[Message 
Body Types]).
 |===
 
 === Example Catalog
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
index 6e99652f6eeb..9fee53d39d85 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
@@ -36,6 +36,7 @@ import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.catalog.EndpointValidationResult;
 import org.apache.camel.catalog.impl.EditDistanceSuggestionStrategy;
+import org.apache.camel.dsl.jbang.core.commands.catalog.KameletDataTypeModel;
 import org.apache.camel.tooling.model.ApiReferenceModel;
 import org.apache.camel.tooling.model.BaseModel;
 import org.apache.camel.tooling.model.BaseOptionModel;
@@ -46,6 +47,7 @@ import org.apache.camel.tooling.model.EipModel;
 import org.apache.camel.tooling.model.JsonMapper;
 import org.apache.camel.tooling.model.LanguageModel;
 import org.apache.camel.tooling.model.PojoBeanModel;
+import org.apache.camel.tooling.model.TransformerModel;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
@@ -443,7 +445,7 @@ public final class CatalogDocs {
         if (kind == null || "kamelet".equals(kind)) {
             KameletDefinitions.Definition def = 
KameletDefinitions.catalog().get(name);
             if (def != null) {
-                return kameletDoc(def);
+                return kameletDoc(def, catalog);
             }
             if (kind != null) {
                 JsonObject err = notFound("Kamelet", name,
@@ -553,6 +555,14 @@ public final class CatalogDocs {
      * bootstrapServers), as it knows those from years of examples.
      */
     public static JsonObject kameletDoc(KameletDefinitions.Definition def) {
+        return kameletDoc(def, null);
+    }
+
+    /**
+     * As {@link #kameletDoc(KameletDefinitions.Definition)}, with the body 
each data type of the Kamelet takes or
+     * gives, as the body types of its data type transformer in the catalog 
say (CAMEL-25928).
+     */
+    public static JsonObject kameletDoc(KameletDefinitions.Definition def, 
CamelCatalog catalog) {
         JsonObject result = new JsonObject();
         result.put("kind", "kamelet");
         result.put("name", def.name());
@@ -595,9 +605,57 @@ public final class CatalogDocs {
             props.add(o);
         }
         result.put("properties", props);
+        if (!def.dataTypes().isEmpty()) {
+            result.put("dataTypes", kameletDataTypes(def, catalog));
+        }
         return result;
     }
 
+    /**
+     * The data types of a Kamelet: the data type transformer of each 
(format), whether it is the default Camel uses,
+     * and the body it takes (in) or gives (out).
+     */
+    private static JsonArray kameletDataTypes(KameletDefinitions.Definition 
def, CamelCatalog catalog) {
+        JsonArray answer = new JsonArray();
+        for (KameletDataTypeModel dt : def.dataTypes()) {
+            JsonObject o = new JsonObject();
+            o.put("direction", dt.direction);
+            o.put("name", dt.name);
+            if (dt.defaultType) {
+                o.put("default", true);
+            }
+            if (dt.format != null) {
+                o.put("format", dt.format);
+            }
+            if (dt.mediaType != null) {
+                o.put("mediaType", dt.mediaType);
+            }
+            if (dt.description != null) {
+                o.put("description", dt.description);
+            }
+            String side = "in".equals(dt.direction) ? 
BodyTypeModel.TRANSFORMER_IN
+                    : "out".equals(dt.direction) ? 
BodyTypeModel.TRANSFORMER_OUT : null;
+            TransformerModel transformer = catalog != null && side != null && 
dt.format != null
+                    ? catalog.transformerModel(transformerName(dt.format)) : 
null;
+            BodyTypeModel body = transformer != null ? 
transformer.getBodyType(side) : null;
+            if (body != null) {
+                o.put("body", bodyTypes(Map.of(side, body)).getString(side));
+            }
+            answer.add(o);
+        }
+        return answer;
+    }
+
+    /**
+     * The name of the data type transformer of a data type, as Camel resolves 
it (TransformerResolver): without the
+     * camel scheme, and with : _ . / and \ as -, so 
aws2-s3:application-cloudevents is aws2-s3-application-cloudevents.
+     */
+    static String transformerName(String dataType) {
+        String name = dataType.startsWith("camel:") ? 
dataType.substring("camel:".length()) : dataType;
+        return name.replace(':', '-').replace('_', '-').replace('.', 
'-').replace('/', '-').replace('\\', '-')
+                .toLowerCase(Locale.ROOT);
+    }
+
     /**
      * The documentation of the EIP a term such as an alias (fan-out, dedup, 
rate-limit) or its whole title names, with
      * the term the caller used; null when no EIP matches. A single word of 
the title is not enough: retry named
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDefinitions.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDefinitions.java
index 7bd79e87574a..f8a30a69f9d4 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDefinitions.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDefinitions.java
@@ -33,6 +33,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Stream;
 
 import org.apache.camel.dsl.jbang.core.commands.catalog.KameletCatalogHelper;
+import org.apache.camel.dsl.jbang.core.commands.catalog.KameletDataTypeModel;
 import org.apache.camel.dsl.jbang.core.commands.catalog.KameletModel;
 import org.apache.camel.dsl.jbang.core.commands.catalog.KameletOptionModel;
 import org.apache.camel.dsl.jbang.core.common.VersionHelper;
@@ -48,9 +49,32 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
  */
 public final class KameletDefinitions {
 
-    /** A Kamelet and its properties. */
+    /** A Kamelet, its properties, and its data types (spec.dataTypes). */
     public record Definition(String name, String type, String title, String 
description, String source,
-            List<Property> properties) {
+            List<Property> properties, List<KameletDataTypeModel> dataTypes) {
+
+        public Definition {
+            dataTypes = dataTypes != null ? List.copyOf(dataTypes) : List.of();
+        }
+
+        /** Without data types. */
+        public Definition(String name, String type, String title, String 
description, String source,
+                          List<Property> properties) {
+            this(name, type, title, description, source, properties, 
List.of());
+        }
+
+        /**
+         * The data type transformer of the default data type of a direction 
(in or out), which Camel runs at the start
+         * or the end of the Kamelet route; null when the Kamelet has none.
+         */
+        public String defaultFormat(String direction) {
+            for (KameletDataTypeModel dt : dataTypes) {
+                if (dt.direction.equals(direction) && dt.defaultType) {
+                    return dt.format;
+                }
+            }
+            return null;
+        }
 
         public Property property(String name) {
             for (Property p : properties) {
@@ -133,7 +157,7 @@ public final class KameletDefinitions {
                         om.enumValues != null ? om.enumValues : List.of()));
             }
         }
-        return new Definition(km.name, km.type, null, km.description, source, 
props);
+        return new Definition(km.name, km.type, null, km.description, source, 
props, km.dataTypes);
     }
 
     /** The project's own Kamelets: the *.kamelet.yaml files of the directory 
and its subdirectories. */
@@ -228,7 +252,38 @@ public final class KameletDefinitions {
                 }
             }
         }
-        return new Definition(name, type, title, description, "the project 
file " + fileName, props);
+        List<KameletDataTypeModel> dataTypes = map.get("spec") instanceof 
Map<?, ?> spec
+                ? dataTypes(spec.get("dataTypes")) : List.of();
+        return new Definition(name, type, title, description, "the project 
file " + fileName, props, dataTypes);
+    }
+
+    /** The data types of a Kamelet file, as the Kamelet catalog reads them 
(KameletCatalogHelper). */
+    private static List<KameletDataTypeModel> dataTypes(Object node) {
+        List<KameletDataTypeModel> answer = new ArrayList<>();
+        if (!(node instanceof Map<?, ?> directions)) {
+            return answer;
+        }
+        for (Map.Entry<?, ?> direction : directions.entrySet()) {
+            if (!(direction.getValue() instanceof Map<?, ?> dt) || 
!(dt.get("types") instanceof Map<?, ?> types)) {
+                continue;
+            }
+            for (Map.Entry<?, ?> type : types.entrySet()) {
+                if (!(type.getValue() instanceof Map<?, ?> t)) {
+                    continue;
+                }
+                String scheme = t.get("scheme") instanceof String sc ? sc : 
null;
+                String format = t.get("format") instanceof String f ? f : null;
+                KameletDataTypeModel model = new KameletDataTypeModel();
+                model.direction = String.valueOf(direction.getKey());
+                model.name = String.valueOf(type.getKey());
+                model.format = format != null && scheme != null ? scheme + ":" 
+ format : format;
+                model.mediaType = t.get("mediaType") instanceof String mt ? mt 
: null;
+                model.description = t.get("description") instanceof String d ? 
d : null;
+                model.defaultType = 
model.name.equals(String.valueOf(dt.get("default")));
+                answer.add(model);
+            }
+        }
+        return answer;
     }
 
     /**
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFacts.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFacts.java
index 735b9a15ffc9..d19e418afd03 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFacts.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFacts.java
@@ -19,8 +19,11 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -53,13 +56,45 @@ final class ValidatorFacts {
      * the file saw at the endpoints of its routes (CAMEL-25750).
      */
     static ProjectFacts read(String fileName, String content, Path directory) {
-        if (content == null || directory == null) {
+        if (content == null) {
             return ProjectFacts.NONE;
         }
+        Map<String, ProjectFacts.KameletDataTypes> kamelets = 
kameletDataTypes(content, directory);
+        if (directory == null) {
+            return new ProjectFacts(Set.of(), Map.of(), null, Map.of(), 
Map.of(), kamelets);
+        }
         return new ProjectFacts(
                 OpenApiVerbs.bodylessEndpoints(content, directory), 
OpenApiVerbs.operations(content, directory),
                 restBindingMode(directory), 
OpenApiVerbs.requestFields(content, directory),
-                ObservedBodies.read(fileName, directory));
+                ObservedBodies.read(fileName, directory), kamelets);
+    }
+
+    /**
+     * The data types of the Kamelets of the catalog and of the project that 
declare them (CAMEL-25928): the data type
+     * transformers of their default in and out data types, which say the body 
a Kamelet takes and gives. Only read when
+     * the file uses a Kamelet.
+     */
+    static Map<String, ProjectFacts.KameletDataTypes> kameletDataTypes(String 
content, Path directory) {
+        if (!content.contains("kamelet")) {
+            return Map.of();
+        }
+        Map<String, ProjectFacts.KameletDataTypes> answer = new HashMap<>();
+        List<Map<String, KameletDefinitions.Definition>> sources = new 
ArrayList<>();
+        sources.add(KameletDefinitions.catalog());
+        if (directory != null) {
+            // the project's own Kamelets win over those of the catalog
+            sources.add(KameletDefinitions.projectKamelets(directory));
+        }
+        for (Map<String, KameletDefinitions.Definition> definitions : sources) 
{
+            for (KameletDefinitions.Definition def : definitions.values()) {
+                String in = def.defaultFormat("in");
+                String out = def.defaultFormat("out");
+                if (in != null || out != null) {
+                    answer.put(def.name(), new 
ProjectFacts.KameletDataTypes(in, out));
+                }
+            }
+        }
+        return answer;
     }
 
     /**
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
index 3c18872c28b8..fd1d56267327 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogDoc.java
@@ -298,7 +298,7 @@ public class CatalogDoc extends CamelCommand {
             // the Kamelets of the version and repositories asked for, not 
only the ones of the CLI
             KameletModel km = KameletCatalogHelper.loadKameletModel(name, 
kameletsVersion, mavenResolver.repos());
             if (km != null) {
-                return CatalogDocs.kameletDoc(KameletDefinitions.of(km, "the 
Kamelet catalog " + kameletsVersion));
+                return CatalogDocs.kameletDoc(KameletDefinitions.of(km, "the 
Kamelet catalog " + kameletsVersion), catalog);
             }
         }
         return null;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletCatalogHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletCatalogHelper.java
index 40d55c6d8c7d..492fbf3a32cd 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletCatalogHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletCatalogHelper.java
@@ -51,6 +51,7 @@ public final class KameletCatalogHelper {
         km.description = getDescription(kamelet);
         if (all) {
             km.dependencies = getDependencies(kamelet);
+            km.dataTypes = getDataTypes(kamelet);
 
             Map<String, Object> props = getProperties(kamelet);
             if (props != null) {
@@ -267,6 +268,46 @@ public final class KameletCatalogHelper {
         return (Map<String, Object>) ObjectHelper.invokeMethod(m, def);
     }
 
+    /**
+     * The data types of the Kamelet (spec.dataTypes): per direction, its 
types, with the default marked. The format of
+     * a type is its scheme and format as Camel joins them to the name of the 
data type transformer
+     * (KameletRoutesBuilderLoader): aws2-s3:application-cloudevents.
+     */
+    @SuppressWarnings("unchecked")
+    private static List<KameletDataTypeModel> getDataTypes(Object kamelet) 
throws Exception {
+        List<KameletDataTypeModel> answer = new ArrayList<>();
+        Method m = kamelet.getClass().getMethod("getSpec");
+        Object spec = ObjectHelper.invokeMethod(m, kamelet);
+        m = spec.getClass().getMethod("getDataTypes");
+        Map<String, Object> dataTypes = (Map<String, Object>) 
ObjectHelper.invokeMethod(m, spec);
+        if (dataTypes == null) {
+            return answer;
+        }
+        for (Map.Entry<String, Object> direction : dataTypes.entrySet()) {
+            Object dt = direction.getValue();
+            String defaultType = (String) 
ObjectHelper.invokeMethod(dt.getClass().getMethod("get_default"), dt);
+            Map<String, Object> types
+                    = (Map<String, Object>) 
ObjectHelper.invokeMethod(dt.getClass().getMethod("getTypes"), dt);
+            if (types == null) {
+                continue;
+            }
+            for (Map.Entry<String, Object> type : types.entrySet()) {
+                Object t = type.getValue();
+                String scheme = (String) 
ObjectHelper.invokeMethod(t.getClass().getMethod("getScheme"), t);
+                String format = (String) 
ObjectHelper.invokeMethod(t.getClass().getMethod("getFormat"), t);
+                KameletDataTypeModel model = new KameletDataTypeModel();
+                model.direction = direction.getKey();
+                model.name = type.getKey();
+                model.format = format != null && scheme != null ? scheme + ":" 
+ format : format;
+                model.mediaType = (String) 
ObjectHelper.invokeMethod(t.getClass().getMethod("getMediaType"), t);
+                model.description = (String) 
ObjectHelper.invokeMethod(t.getClass().getMethod("getDescription"), t);
+                model.defaultType = type.getKey().equals(defaultType);
+                answer.add(model);
+            }
+        }
+        return answer;
+    }
+
     private static List<String> getDependencies(Object kamelet) throws 
Exception {
         List<String> answer = new ArrayList<>();
         Method m = kamelet.getClass().getMethod("getSpec");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletModel.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletDataTypeModel.java
similarity index 61%
copy from 
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletModel.java
copy to 
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletDataTypeModel.java
index 28a4b7965260..6ceadfd9aede 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletModel.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletDataTypeModel.java
@@ -16,16 +16,20 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.catalog;
 
-import java.util.List;
-import java.util.Map;
-
-public class KameletModel {
+/**
+ * A data type of a Kamelet (spec.dataTypes): its format names the data type 
transformer Camel runs for it, as
+ * application-octet-stream or aws2-s3:application-cloudevents.
+ */
+public class KameletDataTypeModel {
 
+    /** in, out or error. */
+    public String direction;
     public String name;
-    public String type;
-    public String supportLevel;
+    /** The data type transformer: the scheme and the format of the type, as 
Camel joins them. */
+    public String format;
+    public String mediaType;
     public String description;
-    public Map<String, KameletOptionModel> properties;
-    public List<String> dependencies;
+    /** Whether it is the default of its direction, which Camel uses unless a 
Pipe chooses another. */
+    public boolean defaultType;
 
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletModel.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletModel.java
index 28a4b7965260..58ee8111eff1 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletModel.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/KameletModel.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.catalog;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -27,5 +28,7 @@ public class KameletModel {
     public String description;
     public Map<String, KameletOptionModel> properties;
     public List<String> dependencies;
+    /** The data types of the Kamelet (spec.dataTypes), in, out and error; 
empty when it has none. */
+    public List<KameletDataTypeModel> dataTypes = new ArrayList<>();
 
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDataTypesTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDataTypesTest.java
new file mode 100644
index 000000000000..4d6b0cdfa33b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletDataTypesTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.dsl.jbang.core.commands.ai;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The data types of a Kamelet say the body it takes and gives, as the body 
types of their data type transformers in the
+ * catalog say (CAMEL-25928).
+ */
+class KameletDataTypesTest {
+
+    private static final CamelCatalog CATALOG = new DefaultCamelCatalog();
+
+    private static final String MY_SOURCE = """
+            apiVersion: camel.apache.org/v1
+            kind: Kamelet
+            metadata:
+              name: my-source
+              labels:
+                camel.apache.org/kamelet.type: source
+            spec:
+              definition:
+                title: My Source
+              dataTypes:
+                out:
+                  default: binary
+                  types:
+                    binary:
+                      format: application-octet-stream
+                      mediaType: application/octet-stream
+                    cloudevents:
+                      scheme: aws2-s3
+                      format: application-cloudevents
+                      description: The CloudEvent headers
+              template:
+                from:
+                  uri: timer:tick
+                  steps:
+                    - setBody:
+                        constant: hello
+                    - to: kamelet:sink
+            """;
+
+    @BeforeAll
+    static void setUp() {
+        // no Kamelet catalog: the project's own Kamelets only
+        KameletDefinitions.setTestCatalog(Map.of());
+    }
+
+    @AfterAll
+    static void tearDown() {
+        KameletDefinitions.setTestCatalog(null);
+    }
+
+    @Test
+    void theDataTypesOfAProjectKamelet(@TempDir Path dir) throws Exception {
+        Files.writeString(dir.resolve("my-source.kamelet.yaml"), MY_SOURCE);
+        KameletDefinitions.Definition def = 
KameletDefinitions.projectKamelets(dir).get("my-source");
+        assertThat(def.dataTypes()).hasSize(2);
+        
assertThat(def.defaultFormat("out")).isEqualTo("application-octet-stream");
+        assertThat(def.defaultFormat("in")).isNull();
+        // the scheme and the format name the transformer, as Camel joins them
+        
assertThat(def.dataTypes().get(1).format).isEqualTo("aws2-s3:application-cloudevents");
+        assertThat(def.dataTypes().get(1).defaultType).isFalse();
+    }
+
+    @Test
+    void theDocSaysTheBodyOfEachDataType(@TempDir Path dir) throws Exception {
+        Files.writeString(dir.resolve("my-source.kamelet.yaml"), MY_SOURCE);
+        JsonObject doc = 
CatalogDocs.kameletDoc(KameletDefinitions.projectKamelets(dir).get("my-source"),
 CATALOG);
+        JsonArray dataTypes = (JsonArray) doc.get("dataTypes");
+        assertThat(dataTypes).hasSize(2);
+        JsonObject binary = (JsonObject) dataTypes.get(0);
+        assertThat(binary.getString("direction")).isEqualTo("out");
+        
assertThat(binary.getString("format")).isEqualTo("application-octet-stream");
+        assertThat(binary.getBoolean("default")).isTrue();
+        assertThat(binary.getString("body")).startsWith("bytes");
+        assertThat(((JsonObject) 
dataTypes.get(1)).getString("body")).startsWith("unchanged");
+        // without the catalog the doc has the data types, but not their body
+        JsonObject plain = 
CatalogDocs.kameletDoc(KameletDefinitions.projectKamelets(dir).get("my-source"));
+        assertThat(((JsonObject) ((JsonArray) 
plain.get("dataTypes")).get(0)).get("body")).isNull();
+    }
+
+    @Test
+    void theValidatorWalksTheBodyOfTheDataType(@TempDir Path dir) throws 
Exception {
+        Files.writeString(dir.resolve("my-source.kamelet.yaml"), MY_SOURCE);
+        String route = """
+                - route:
+                    from:
+                      uri: kamelet:my-source
+                      steps:
+                        - setHeader:
+                            name: status
+                            simple: "${body[status]}"
+                """;
+        List<String> messages = SourceValidator.validate("orders.camel.yaml", 
route, CATALOG, null, dir);
+        assertThat(messages).anyMatch(m -> m.contains("simple reads a field of 
the body (${body[status]})")
+                && m.contains("kamelet:my-source gives it as its out data type 
application-octet-stream"));
+        assertThat(ValidatorFacts.kameletDataTypes(route, dir))
+                .containsKey("my-source")
+                .doesNotContainKey("other");
+        // a file that uses no Kamelet does not read them
+        assertThat(ValidatorFacts.kameletDataTypes("- route: {from: {uri: 
timer:tick}}", dir)).isEmpty();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
index 278c581cc6fd..a3ed06b2630d 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
@@ -23,14 +23,19 @@ import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
 import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
 
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.dsl.jbang.core.commands.ai.CatalogDocs;
+import org.apache.camel.dsl.jbang.core.commands.ai.KameletDefinitions;
 import org.apache.camel.dsl.jbang.core.commands.catalog.KameletCatalogHelper;
 import org.apache.camel.dsl.jbang.core.commands.catalog.KameletModel;
 import org.apache.camel.dsl.jbang.core.commands.catalog.KameletOptionModel;
 import org.apache.camel.dsl.jbang.core.common.VersionHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
 
 /**
  * MCP Tools for querying the Kamelet Catalog using Quarkus MCP Server.
@@ -39,6 +44,9 @@ import org.apache.camel.dsl.jbang.core.common.VersionHelper;
 @ApplicationScoped
 public class KameletTools {
 
+    @Inject
+    CatalogService catalogService;
+
     /**
      * Cache of loaded Kamelet catalogs keyed by version string. Avoids 
recreating a new downloader and class-loader on
      * every request (CAMEL-25640).
@@ -158,13 +166,30 @@ public class KameletTools {
                 }
             }
 
+            // the body each data type takes or gives, as its data type 
transformer in the catalog says (CAMEL-25928)
+            List<KameletDataTypeInfo> dataTypes = new ArrayList<>();
+            if (km.dataTypes != null && !km.dataTypes.isEmpty()) {
+                JsonObject doc = 
CatalogDocs.kameletDoc(KameletDefinitions.of(km, "the Kamelet catalog " + 
version),
+                        catalogService != null ? 
catalogService.getDefaultCatalog() : null);
+                if (doc.get("dataTypes") instanceof JsonArray array) {
+                    for (Object o : array) {
+                        JsonObject dt = (JsonObject) o;
+                        dataTypes.add(new KameletDataTypeInfo(
+                                dt.getString("direction"), 
dt.getString("name"), dt.getString("format"),
+                                dt.getBooleanOrDefault("default", false), 
dt.getString("mediaType"),
+                                dt.getString("description"), 
dt.getString("body")));
+                    }
+                }
+            }
+
             return new KameletDetailResult(
                     km.name,
                     km.type,
                     km.supportLevel,
                     km.description,
                     options,
-                    km.dependencies);
+                    km.dependencies,
+                    dataTypes.isEmpty() ? null : dataTypes);
         } catch (ToolCallException e) {
             throw e;
         } catch (Throwable e) {
@@ -183,7 +208,15 @@ public class KameletTools {
     }
 
     public record KameletDetailResult(String name, String type, String 
supportLevel, String description,
-            List<KameletOptionInfo> options, List<String> dependencies) {
+            List<KameletOptionInfo> options, List<String> dependencies, 
List<KameletDataTypeInfo> dataTypes) {
+    }
+
+    /**
+     * A data type of the Kamelet: in, out or error, its data type transformer 
(format), whether Camel uses it by
+     * default, and the body it takes (in) or gives (out).
+     */
+    public record KameletDataTypeInfo(String direction, String name, String 
format, boolean defaultType,
+            String mediaType, String description, String body) {
     }
 
     public record KameletOptionInfo(String name, String description, String 
type, boolean required,
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletToolsTest.java
index ad9c0830b9e8..c0224eeb1da7 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletToolsTest.java
@@ -122,4 +122,21 @@ class KameletToolsTest {
         assertThat(r1.kamelets()).isEqualTo(r2.kamelets());
         assertThat(r1.kameletsVersion()).isEqualTo(r2.kameletsVersion());
     }
+
+    /** CAMEL-25928: the data types of a Kamelet, and the body their data type 
transformer gives. */
+    @Test
+    void theDataTypesOfAKamelet() {
+        tools.catalogService = new CatalogService();
+        KameletTools.KameletDetailResult s3 = 
tools.camel_catalog_kamelet_doc("aws-s3-source", null);
+        assertThat(s3.dataTypes()).isNotEmpty();
+        KameletTools.KameletDataTypeInfo binary = s3.dataTypes().stream()
+                
.filter(KameletTools.KameletDataTypeInfo::defaultType).findFirst().orElseThrow();
+        assertThat(binary.direction()).isEqualTo("out");
+        assertThat(binary.format()).isEqualTo("application-octet-stream");
+        assertThat(binary.body()).startsWith("bytes");
+        assertThat(s3.dataTypes()).anyMatch(dt -> 
"aws2-s3:application-cloudevents".equals(dt.format())
+                && dt.body().startsWith("unchanged"));
+        // a Kamelet without data types has none
+        assertThat(tools.camel_catalog_kamelet_doc("log-sink", 
null).dataTypes()).isNull();
+    }
 }
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 264ac9807a71..122595a70cf0 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
@@ -194,9 +194,26 @@ final class CatalogBodyTypes {
         return transformerSide(step, BodyTypeModel.TRANSFORMER_OUT);
     }
 
+    /**
+     * The bodies the data type transformer of a data type can transform, such 
as the in data type of a Kamelet; null
+     * when it is not known.
+     */
+    Answer transformerIn(String dataType) {
+        return transformerSide(dataType, BodyTypeModel.TRANSFORMER_IN);
+    }
+
+    /** The body after the data type transformer of a data type, such as the 
out data type of a Kamelet. */
+    Answer transformerOut(String dataType) {
+        return transformerSide(dataType, BodyTypeModel.TRANSFORMER_OUT);
+    }
+
     private Answer transformerSide(JsonNode step, String side) {
-        String toType = step != null && step.isObject() && step.has("toType") 
? step.get("toType").asText() : null;
-        TransformerModel model = transformer(toType);
+        return transformerSide(step != null && step.isObject() && 
step.has("toType") ? step.get("toType").asText() : null,
+                side);
+    }
+
+    private Answer transformerSide(String dataType, String side) {
+        TransformerModel model = transformer(dataType);
         return model != null ? answer(model.getBodyType(side), Map.of(), 
Set.of()) : null;
     }
 
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/ProjectFacts.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/ProjectFacts.java
index 01b05c33b975..e84ad2d8fab1 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/ProjectFacts.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/ProjectFacts.java
@@ -37,9 +37,12 @@ import java.util.Set;
  * @param observed          what a running integration of the routes saw at 
their endpoints (CAMEL-25750): the body a
  *                          consumer gave, keyed as {@link 
#consumerKey(String)}, and the body a producer returned,
  *                          keyed as {@link #producerKey(String)}: the types 
the step after it was reached with
+ * @param kamelets          the data types of the Kamelets the routes use, by 
name (CAMEL-25928): the data type
+ *                          transformers Camel runs at the start and the end 
of their route
  */
 public record ProjectFacts(Set<String> bodylessEndpoints, Map<String, 
Map<String, String>> operations,
-        String restBindingMode, Map<String, Map<String, List<String>>> 
requestFields, Map<String, Observed> observed) {
+        String restBindingMode, Map<String, Map<String, List<String>>> 
requestFields, Map<String, Observed> observed,
+        Map<String, KameletDataTypes> kamelets) {
 
     /** Nothing known about the project. */
     public static final ProjectFacts NONE = new ProjectFacts(Set.of(), 
Map.of(), null);
@@ -56,11 +59,26 @@ public record ProjectFacts(Set<String> bodylessEndpoints, 
Map<String, Map<String
         }
     }
 
+    /**
+     * The data types of a Kamelet: the data type transformer of its default 
in and out data type (spec.dataTypes), as
+     * application-octet-stream or aws2-s3:application-cloudevents; null for a 
direction it has none for.
+     */
+    public record KameletDataTypes(String in, String out) {
+    }
+
     public ProjectFacts {
         bodylessEndpoints = bodylessEndpoints != null ? bodylessEndpoints : 
Set.of();
         operations = operations != null ? operations : Map.of();
         requestFields = requestFields != null ? requestFields : Map.of();
         observed = observed != null ? observed : Map.of();
+        kamelets = kamelets != null ? kamelets : Map.of();
+    }
+
+    /** Without the data types of the Kamelets. */
+    public ProjectFacts(Set<String> bodylessEndpoints, Map<String, Map<String, 
String>> operations,
+                        String restBindingMode, Map<String, Map<String, 
List<String>>> requestFields,
+                        Map<String, Observed> observed) {
+        this(bodylessEndpoints, operations, restBindingMode, requestFields, 
observed, Map.of());
     }
 
     /** Without what a running integration saw. */
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
index d318fbda5177..0dd20624d29a 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
@@ -175,11 +175,12 @@ final class TextBodyFlow {
      */
     private record Walk(String routeId, Map<String, JsonNode> byFrom, 
List<Error> errors, Set<String> seen, boolean post,
             Map<String, Map<String, String>> operations, Map<String, 
Map<String, List<String>>> requestFields,
-            Map<String, ProjectFacts.Observed> observed, CatalogBodyTypes 
bodyTypes) {
+            Map<String, ProjectFacts.Observed> observed, CatalogBodyTypes 
bodyTypes,
+            Map<String, ProjectFacts.KameletDataTypes> kamelets) {
 
         /** The walk of a route a step calls: for its outcome alone. */
         Walk called() {
-            return new Walk(null, byFrom, null, seen, false, operations, 
requestFields, observed, bodyTypes);
+            return new Walk(null, byFrom, null, seen, false, operations, 
requestFields, observed, bodyTypes, kamelets);
         }
     }
 
@@ -193,6 +194,7 @@ final class TextBodyFlow {
         Map<String, Map<String, List<String>>> requestFields = unbound ? 
facts.requestFields() : Map.of();
         Set<String> restReplies = unbound ? restReplies(target, facts) : 
Set.of();
         Map<String, ProjectFacts.Observed> observed = facts != null ? 
facts.observed() : Map.of();
+        Map<String, ProjectFacts.KameletDataTypes> kamelets = facts != null ? 
facts.kamelets() : Map.of();
         Map<String, JsonNode> byFrom = new HashMap<>();
         for (JsonNode entry : target) {
             JsonNode from = from(entry);
@@ -226,14 +228,17 @@ final class TextBodyFlow {
             boolean post = NO_HTTP_HEADERS.contains(consumer) && 
!setsHttpHeaders(steps);
             // with variableReceive the message goes into the variable, and 
the route starts with no body
             boolean variable = bodyTypes.startsWithNoBody(from);
-            Body start = uri != null && !variable ? consumerBody(uri, from, 
bodyTypes) : null;
+            Body start = uri == null || variable ? null
+                    : "kamelet".equals(consumer)
+                            ? kameletBody(kameletName(uri), kamelets, 
bodyTypes, "from: " + normalize(uri))
+                    : consumerBody(uri, from, bodyTypes);
             if (start == null && uri != null && !variable) {
                 // what the consumer gave in a running integration of the route
                 start = observed(observed, ProjectFacts.consumerKey(uri), 
"from: " + normalize(uri) + " gave");
             }
             Body end = walk(steps, start, at, new Walk(
                     id, byFrom, errors, new HashSet<>(), post, operations,
-                    requestFields, observed, bodyTypes));
+                    requestFields, observed, bodyTypes, kamelets));
             // the body at the end of the route of a consumer that replies 
with text (platform-http), or of the route of
             // a REST call, is the reply
             boolean rest = uri != null && restReplies.contains(normalize(uri));
@@ -432,8 +437,16 @@ final class TextBodyFlow {
                 }
                 return producerBody(out, "transformDataType: " + 
toTypeOf(value) + " makes it");
             }
+            case "kamelet" -> {
+                // what the Kamelet leaves, as the transformer of its out data 
type says
+                String kamelet = kameletName(value);
+                return kamelet != null ? kameletAfter(kamelet, body, w, 
"kamelet: " + kamelet) : null;
+            }
             case "to", "toD" -> {
                 String uri = endpointOf(value);
+                if ("kamelet".equals(scheme(uri))) {
+                    return kameletAfter(kameletName(uri), body, w, name + ": " 
+ normalize(uri));
+                }
                 CatalogBodyTypes.Answer out = uri != null ? 
w.bodyTypes().producerOut(uri, value) : null;
                 if (out != null && out.unchanged()) {
                     // log, a file write
@@ -683,6 +696,51 @@ final class TextBodyFlow {
     private record Operation(String spec, String id) {
     }
 
+    /**
+     * The name of the Kamelet of a kamelet: endpoint (aws-s3-source of 
kamelet:aws-s3-source?bucketNameOrArn=x, or of
+     * kamelet:my/route), or of a kamelet step ({@code kamelet: my-action} or 
with a name option).
+     */
+    private static String kameletName(String uri) {
+        if (uri == null || !uri.startsWith("kamelet:")) {
+            return null;
+        }
+        String name = uri.substring("kamelet:".length());
+        for (char c : new char[] { '?', '/' }) {
+            int i = name.indexOf(c);
+            if (i >= 0) {
+                name = name.substring(0, i);
+            }
+        }
+        return name.isBlank() || name.contains("{{") ? null : name;
+    }
+
+    private static String kameletName(JsonNode value) {
+        JsonNode name = value != null && value.isObject() ? value.get("name") 
: value;
+        return name != null && name.isTextual() ? kameletName("kamelet:" + 
name.asText()) : null;
+    }
+
+    /** The body a Kamelet gives, as the transformer of its out data type 
says; null when it is not known. */
+    private static Body kameletBody(
+            String kamelet, Map<String, ProjectFacts.KameletDataTypes> 
kamelets, CatalogBodyTypes bodyTypes,
+            String origin) {
+        ProjectFacts.KameletDataTypes types = kamelet != null ? 
kamelets.get(kamelet) : null;
+        if (types == null || types.out() == null) {
+            return null;
+        }
+        return producerBody(bodyTypes.transformerOut(types.out()),
+                origin + " gives it as its out data type " + types.out());
+    }
+
+    /** The body after a step that runs a Kamelet: its out data type may keep 
the body, as the CloudEvent ones do. */
+    private static Body kameletAfter(String kamelet, Body body, Walk w, String 
origin) {
+        ProjectFacts.KameletDataTypes types = kamelet != null ? 
w.kamelets().get(kamelet) : null;
+        CatalogBodyTypes.Answer out = types != null && types.out() != null ? 
w.bodyTypes().transformerOut(types.out()) : null;
+        if (out != null && out.unchanged()) {
+            return body;
+        }
+        return kameletBody(kamelet, w.kamelets(), w.bodyTypes(), origin);
+    }
+
     private static String toTypeOf(JsonNode value) {
         return value != null && value.isObject() && value.has("toType") ? 
value.get("toType").asText() : "";
     }
@@ -694,6 +752,21 @@ final class TextBodyFlow {
      * producer sends the body, and a REST binding may write it for 
rest-openapi: they keep their own rule.
      */
     private static Need needsText(String name, JsonNode value, Walk w) {
+        String kamelet = "kamelet".equals(name) ? kameletName(value)
+                : ("to".equals(name) || "toD".equals(name)) && 
"kamelet".equals(scheme(endpointOf(value)))
+                        ? kameletName(endpointOf(value))
+                : null;
+        if (kamelet != null) {
+            // what the transformer of the in data type of the Kamelet takes
+            ProjectFacts.KameletDataTypes types = w.kamelets().get(kamelet);
+            CatalogBodyTypes.Answer in = types != null && types.in() != null ? 
w.bodyTypes().transformerIn(types.in()) : null;
+            return in != null && in.onlyPayload()
+                    ? new Need(
+                            name + ": the Kamelet " + kamelet + " needs the 
body as " + in.inWords() + " (its in data type "
+                               + types.in() + ")",
+                            "before this step")
+                    : null;
+        }
         if ("transformDataType".equals(name)) {
             CatalogBodyTypes.Answer in = w.bodyTypes().transformerIn(value);
             return in != null && in.onlyPayload()
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
index 5e47b7207fb5..8debd489f14a 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
@@ -2508,4 +2508,78 @@ public class TextBodyFlowTest {
                         .contains(
                                 "transformDataType: 
camel:application-octet-stream needs the body as text, bytes, a stream or a 
file"));
     }
+
+    /** The data types of the Kamelets of a project: the transformers of their 
default in and out data type. */
+    private static final ProjectFacts KAMELETS = new ProjectFacts(
+            Set.of(), Map.of(), null, Map.of(), Map.of(),
+            Map.of("my-source", new ProjectFacts.KameletDataTypes(null, 
"application-octet-stream"),
+                    "my-sink", new 
ProjectFacts.KameletDataTypes("application-octet-stream", null),
+                    "my-action", new 
ProjectFacts.KameletDataTypes("application-x-struct", "text-plain"),
+                    "my-events", new ProjectFacts.KameletDataTypes(null, 
"aws2-s3:application-cloudevents")));
+
+    @Test
+    public void testAKameletSourceGivesItsOutDataType() {
+        // application-octet-stream gives bytes: a field of the body cannot be 
read
+        String yaml = """
+                - route:
+                    from:
+                      uri: kamelet:my-source?bucket=orders
+                      steps:
+                        - setHeader:
+                            name: status
+                            simple: "${body[status]}"
+                """;
+        assertThat(lines(yaml, KAMELETS)).singleElement()
+                .satisfies(m -> assertThat(m).contains("simple reads a field 
of the body (${body[status]})"));
+        // a Kamelet without data types says nothing
+        assertThat(lines(yaml.replace("my-source", "other-source"), 
KAMELETS)).isEmpty();
+    }
+
+    @Test
+    public void testAKameletSinkNeedsItsInDataType() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: file:inbox
+                      steps:
+                        - unmarshal:
+                            json: {}
+                        - to:
+                            uri: kamelet:my-sink
+                """;
+        assertThat(lines(yaml, KAMELETS)).singleElement()
+                .satisfies(m -> assertThat(m).contains(
+                        "to: the Kamelet my-sink needs the body as text, 
bytes, a stream or a file (its in data type"
+                                                       + " 
application-octet-stream)"));
+    }
+
+    @Test
+    public void testAKameletStepLeavesItsOutDataType() {
+        // text-plain makes the parsed data text again
+        String yaml = """
+                - route:
+                    from:
+                      uri: file:inbox
+                      steps:
+                        - unmarshal:
+                            json: {}
+                        - kamelet:
+                            name: my-action
+                        - to: file:outbox
+                """;
+        assertThat(lines(yaml, KAMELETS)).isEmpty();
+        // a CloudEvent data type sets headers only: the body is still parsed 
data
+        String events = """
+                - route:
+                    from:
+                      uri: file:inbox
+                      steps:
+                        - unmarshal:
+                            json: {}
+                        - to: kamelet:my-events
+                        - to: file:outbox
+                """;
+        assertThat(lines(events, KAMELETS)).singleElement()
+                .satisfies(m -> assertThat(m).contains("to: file needs the 
body as text"));
+    }
 }

Reply via email to