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

davsclaus pushed a commit to branch CAMEL-25928-kamelet-data-types
in repository https://gitbox.apache.org/repos/asf/camel.git

commit d802b8bc5fe6e214c448d46b112c71b7f67cfe6f
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 25 13:55:05 2026 +0100

    CAMEL-25928: camel-jbang - the body of a Kamelet from its data types
    
    A Kamelet's data types (spec.dataTypes) name the data type transformers 
Camel
    runs at the start and the end of its route, so their body types say the body
    the Kamelet takes and gives. The Camel CLI reads them from the Kamelet 
catalog
    and the project's own Kamelets:
    
    - the YAML validator starts a route of from: kamelet:x with the body of its
      out data type, and reports a body that the in data type of a to: kamelet:x
      or a kamelet step cannot take;
    - camel_catalog_kamelet_doc and camel_catalog_doc list the data types of a
      Kamelet with the body of each.
    
    A Kamelet without data types says nothing, as before.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[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 d2c359b3a3bd..e7de780194de 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