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 9e2b7ec3ba11 CAMEL-26512: camel-spring-ai-embeddings - fail a Map or 
POJO body, or List element, instead of embedding its toString() (#29108)
9e2b7ec3ba11 is described below

commit 9e2b7ec3ba11c35b31d224cb7ee5339cd58721b4
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 4 19:59:46 2026 +0100

    CAMEL-26512: camel-spring-ai-embeddings - fail a Map or POJO body, or List 
element, instead of embedding its toString() (#29108)
    
    - CAMEL-26512: camel-spring-ai-embeddings - fail a Map or POJO body, or 
List element, instead of embedding its toString()
    - CAMEL-26512: camel-spring-ai-embeddings - the batch doc example sends a 
List in XML and YAML
    - CAMEL-26512: camel-spring-ai-embeddings - name the char[], ByteBuf and 
converter bodies in the upgrade guide, and say only a java.util.List is batched
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../catalog/components/spring-ai-embeddings.json   |   2 +-
 .../docs/spring-ai-embeddings-component.adoc       |  11 +-
 .../springai/embeddings/spring-ai-embeddings.json  |   2 +-
 .../main/docs/spring-ai-embeddings-component.adoc  |  11 +-
 .../embeddings/SpringAiEmbeddingsEndpoint.java     |  10 +-
 .../embeddings/SpringAiEmbeddingsProducer.java     |  82 ++++++++--
 .../SpringAiEmbeddingsComponentTest.java           | 180 ++++++++++++++++++++-
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  19 ++-
 8 files changed, 281 insertions(+), 36 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-ai-embeddings.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-ai-embeddings.json
index e182c253f1ef..d918a7f2730e 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-ai-embeddings.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/spring-ai-embeddings.json
@@ -38,7 +38,7 @@
     "CamelSpringAiEmbeddingInputTexts": { "index": 5, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.List<String>", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "List of input texts 
that were embedded", "constantName": 
"org.apache.camel.component.springai.embeddings.SpringAiEmbeddingsHeaders#INPUT_TEXTS"
 }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "any" ], "description": "The text to embed, 
or a List of texts for a batch. Another body, and each element of a List, is 
converted to text (a Map or a POJO as its toString()); no body, or a null 
element in a List, fails" },
+    "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "list" ], 
"description": "The text to embed: a String, bytes, a stream or a file read as 
text, or a Spring AI Document as its text; or a java.util.List of them for a 
batch (a Set or an array of them fails). Another body or List element, such as 
a Map or a POJO, fails, as do no body and a null element: convert it to text 
first" },
     "producerOut": { "carriers": [ "list" ], "description": "The vector as a 
float[] for one text, and a List of float[] for a List of texts, also for a 
List with one text or none. The Embedding and the input text are in headers, 
which spring-ai-vector-store reads" }
   },
   "properties": {
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-embeddings-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-embeddings-component.adoc
index 99ab8537b98c..a2036cd24c7f 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-embeddings-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/spring-ai-embeddings-component.adoc
@@ -50,7 +50,10 @@ The component accepts two types of input:
 * **Single text**: A String containing the text to embed
 * **Batch processing**: A List<String> containing multiple texts to embed at 
once
 
-Another body, and each element of a List, is converted to a String. A `null` 
element in a List fails the exchange
+Bytes, a stream, a `Reader` or a file is read as text, and a Spring AI 
`Document` is embedded as its text, as a body
+and as an element of a List. Any other body or List element, such as a `Map`, 
a POJO or a number, fails the exchange
+with an `InvalidPayloadException` instead of being embedded as its 
`toString()`: turn it into text first, with
+`convertBodyTo(String.class)` or a marshal to JSON, say. No body, or a `null` 
element in a List, fails the exchange
 with an `IllegalArgumentException`.
 
 The embedding vectors are returned in the message body, with additional 
metadata available in headers.
@@ -124,7 +127,7 @@ XML::
 <route>
   <from uri="direct:batch"/>
   <setBody>
-    <constant>["text 1", "text 2", "text 3"]</constant>
+    <simple>${list(text 1,text 2,text 3)}</simple>
   </setBody>
   <to uri="spring-ai-embeddings:myEmbedding"/>
   <log message="Number of embeddings: ${body.size()}"/>
@@ -140,7 +143,9 @@ YAML::
       uri: direct:batch
       steps:
         - setBody:
-            constant: '["text 1", "text 2", "text 3"]'
+            expression:
+              simple:
+                expression: "${list(text 1,text 2,text 3)}"
         - to:
             uri: spring-ai-embeddings:myEmbedding
         - log:
diff --git 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/generated/resources/META-INF/org/apache/camel/component/springai/embeddings/spring-ai-embeddings.json
 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/generated/resources/META-INF/org/apache/camel/component/springai/embeddings/spring-ai-embeddings.json
index e182c253f1ef..d918a7f2730e 100644
--- 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/generated/resources/META-INF/org/apache/camel/component/springai/embeddings/spring-ai-embeddings.json
+++ 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/generated/resources/META-INF/org/apache/camel/component/springai/embeddings/spring-ai-embeddings.json
@@ -38,7 +38,7 @@
     "CamelSpringAiEmbeddingInputTexts": { "index": 5, "kind": "header", 
"displayName": "", "group": "producer", "label": "", "required": false, 
"javaType": "java.util.List<String>", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "List of input texts 
that were embedded", "constantName": 
"org.apache.camel.component.springai.embeddings.SpringAiEmbeddingsHeaders#INPUT_TEXTS"
 }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "any" ], "description": "The text to embed, 
or a List of texts for a batch. Another body, and each element of a List, is 
converted to text (a Map or a POJO as its toString()); no body, or a null 
element in a List, fails" },
+    "producerIn": { "carriers": [ "text", "bytes", "stream", "file", "list" ], 
"description": "The text to embed: a String, bytes, a stream or a file read as 
text, or a Spring AI Document as its text; or a java.util.List of them for a 
batch (a Set or an array of them fails). Another body or List element, such as 
a Map or a POJO, fails, as do no body and a null element: convert it to text 
first" },
     "producerOut": { "carriers": [ "list" ], "description": "The vector as a 
float[] for one text, and a List of float[] for a List of texts, also for a 
List with one text or none. The Embedding and the input text are in headers, 
which spring-ai-vector-store reads" }
   },
   "properties": {
diff --git 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/docs/spring-ai-embeddings-component.adoc
 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/docs/spring-ai-embeddings-component.adoc
index 99ab8537b98c..a2036cd24c7f 100644
--- 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/docs/spring-ai-embeddings-component.adoc
+++ 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/docs/spring-ai-embeddings-component.adoc
@@ -50,7 +50,10 @@ The component accepts two types of input:
 * **Single text**: A String containing the text to embed
 * **Batch processing**: A List<String> containing multiple texts to embed at 
once
 
-Another body, and each element of a List, is converted to a String. A `null` 
element in a List fails the exchange
+Bytes, a stream, a `Reader` or a file is read as text, and a Spring AI 
`Document` is embedded as its text, as a body
+and as an element of a List. Any other body or List element, such as a `Map`, 
a POJO or a number, fails the exchange
+with an `InvalidPayloadException` instead of being embedded as its 
`toString()`: turn it into text first, with
+`convertBodyTo(String.class)` or a marshal to JSON, say. No body, or a `null` 
element in a List, fails the exchange
 with an `IllegalArgumentException`.
 
 The embedding vectors are returned in the message body, with additional 
metadata available in headers.
@@ -124,7 +127,7 @@ XML::
 <route>
   <from uri="direct:batch"/>
   <setBody>
-    <constant>["text 1", "text 2", "text 3"]</constant>
+    <simple>${list(text 1,text 2,text 3)}</simple>
   </setBody>
   <to uri="spring-ai-embeddings:myEmbedding"/>
   <log message="Number of embeddings: ${body.size()}"/>
@@ -140,7 +143,9 @@ YAML::
       uri: direct:batch
       steps:
         - setBody:
-            constant: '["text 1", "text 2", "text 3"]'
+            expression:
+              simple:
+                expression: "${list(text 1,text 2,text 3)}"
         - to:
             uri: spring-ai-embeddings:myEmbedding
         - log:
diff --git 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsEndpoint.java
 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsEndpoint.java
index e82a65c38365..3276d8b93391 100644
--- 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsEndpoint.java
+++ 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsEndpoint.java
@@ -37,10 +37,12 @@ import org.apache.camel.support.DefaultEndpoint;
              syntax = "spring-ai-embeddings:embeddingId", producerOnly = true, 
category = {
                      Category.AI
              }, headersClass = SpringAiEmbeddingsHeaders.class)
-@BodyTypes(producerIn = @BodyType(value = Carrier.ANY,
-                                  description = "The text to embed, or a List 
of texts for a batch. Another body, and"
-                                                + " each element of a List, is 
converted to text (a Map or a POJO as its"
-                                                + " toString()); no body, or a 
null element in a List, fails"),
+@BodyTypes(producerIn = @BodyType(value = { Carrier.TEXT, Carrier.BYTES, 
Carrier.STREAM, Carrier.FILE, Carrier.LIST },
+                                  description = "The text to embed: a String, 
bytes, a stream or a file read as text, or a"
+                                                + " Spring AI Document as its 
text; or a java.util.List of them for a batch (a"
+                                                + " Set or an array of them 
fails). Another body or List element, such as a"
+                                                + " Map or a POJO, fails, as 
do no body and a null element: convert it to"
+                                                + " text first"),
            producerOut = @BodyType(value = Carrier.LIST,
                                    description = "The vector as a float[] for 
one text, and a List of float[] for a List"
                                                  + " of texts, also for a List 
with one text or none. The Embedding and"
diff --git 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsProducer.java
 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsProducer.java
index a3a64b8710ea..4f9d1bbda7d6 100644
--- 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsProducer.java
+++ 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/main/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsProducer.java
@@ -16,12 +16,23 @@
  */
 package org.apache.camel.component.springai.embeddings;
 
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.ByteBuffer;
+import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.List;
 
 import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
 import org.apache.camel.Message;
+import org.apache.camel.NoTypeConversionAvailableException;
+import org.apache.camel.StreamCache;
+import org.apache.camel.WrappedFile;
 import org.apache.camel.support.DefaultProducer;
+import org.springframework.ai.document.Document;
 import org.springframework.ai.embedding.Embedding;
 import org.springframework.ai.embedding.EmbeddingModel;
 import org.springframework.ai.embedding.EmbeddingOptions;
@@ -43,7 +54,7 @@ public class SpringAiEmbeddingsProducer extends 
DefaultProducer {
         final Message message = exchange.getMessage();
         final EmbeddingModel model = 
getEndpoint().getConfiguration().getEmbeddingModel();
 
-        // Support both single String and List<String> as input
+        // Support a single text and a List of texts as input
         List<String> inputTexts;
         Object body = message.getBody();
         boolean batch = body instanceof List;
@@ -52,14 +63,10 @@ public class SpringAiEmbeddingsProducer extends 
DefaultProducer {
             inputTexts = new ArrayList<>(list.size());
             for (int i = 0; i < list.size(); i++) {
                 Object element = list.get(i);
-                String text = element != null
-                        ? 
exchange.getContext().getTypeConverter().convertTo(String.class, exchange, 
element) : null;
-                if (text == null) {
-                    throw new IllegalArgumentException(
-                            "Element " + i + " of the List body must be text, 
but was: "
-                                                       + (element != null ? 
element.getClass().getName() : "null"));
+                if (element == null) {
+                    throw new IllegalArgumentException("Element " + i + " of 
the List body must be text, but was: null");
                 }
-                inputTexts.add(text);
+                inputTexts.add(toText(exchange, element, i));
             }
             if (inputTexts.isEmpty()) {
                 // nothing to embed, so do not call the model
@@ -69,14 +76,10 @@ public class SpringAiEmbeddingsProducer extends 
DefaultProducer {
                 return;
             }
         } else {
-            String str = message.getBody(String.class);
-            if (str != null) {
-                inputTexts = List.of(str);
-            } else {
-                throw new IllegalArgumentException(
-                        "Message body must be a String or List<String>, but 
was: "
-                                                   + (body != null ? 
body.getClass().getName() : "null"));
+            if (body == null) {
+                throw new IllegalArgumentException("Message body must be a 
String or List<String>, but was: null");
             }
+            inputTexts = List.of(toText(exchange, body, -1));
         }
 
         final EmbeddingResponse response = model.call(new EmbeddingRequest(
@@ -109,4 +112,53 @@ public class SpringAiEmbeddingsProducer extends 
DefaultProducer {
             message.setHeader(SpringAiEmbeddingsHeaders.EMBEDDING_METADATA, 
response.getMetadata());
         }
     }
+
+    /**
+     * The text to embed of a body, or of an element of a List body: a Spring 
AI Document as its text, and text, bytes,
+     * a stream, a Reader or a file read as text. Any other value, such as a 
Map or a POJO, fails as an
+     * InvalidPayloadException instead of being embedded as its toString().
+     *
+     * @param index the index of the element in a List body, or -1 for the 
body itself
+     */
+    private static String toText(Exchange exchange, Object value, int index)
+            throws InvalidPayloadException, NoTypeConversionAvailableException 
{
+        if (value instanceof Document document) {
+            if (document.getText() == null) {
+                throw new InvalidPayloadException(
+                        exchange, String.class, exchange.getMessage(),
+                        listElement(value,
+                                index) + "spring-ai-embeddings embeds the text 
of a Document, but the Document has no text");
+            }
+            return document.getText();
+        }
+        if (!isReadableAsText(value)) {
+            throw new InvalidPayloadException(
+                    exchange, String.class, exchange.getMessage(),
+                    listElement(value,
+                            index) + "spring-ai-embeddings embeds text, bytes, 
a stream, a Reader, a file or a Spring AI Document;"
+                                                                   + " convert 
the body to text before the endpoint, such"
+                                                                   + " as with 
convertBodyTo(String) or a marshal to JSON");
+        }
+        return 
exchange.getContext().getTypeConverter().mandatoryConvertTo(String.class, 
exchange, value);
+    }
+
+    /** A value Camel converts to its text, rather than to the toString() of 
an object. */
+    private static boolean isReadableAsText(Object value) {
+        return value instanceof CharSequence || value instanceof byte[] || 
value instanceof ByteBuffer
+                || value instanceof InputStream || value instanceof Reader || 
value instanceof StreamCache
+                || value instanceof ByteArrayOutputStream
+                || value instanceof File || value instanceof Path || value 
instanceof WrappedFile;
+    }
+
+    /**
+     * Where a value that fails is in the body, for the message of the 
exception: the index and the type of an element
+     * of a List body, or nothing for the body itself. Only built when the 
value fails.
+     */
+    private static String listElement(Object value, int index) {
+        if (index < 0) {
+            return "";
+        }
+        return "the element at index " + index + " of the List body is "
+               + (value == null ? "null" : value.getClass().getName()) + "; ";
+    }
 }
diff --git 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/test/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsComponentTest.java
 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/test/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsComponentTest.java
index 61472572c4a4..aa4222f0117f 100644
--- 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/test/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsComponentTest.java
+++ 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-embeddings/src/test/java/org/apache/camel/component/springai/embeddings/SpringAiEmbeddingsComponentTest.java
@@ -16,22 +16,31 @@
  */
 package org.apache.camel.component.springai.embeddings;
 
+import java.io.ByteArrayInputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
 import org.apache.camel.Message;
+import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.test.junit6.CamelTestSupport;
 import org.junit.jupiter.api.Test;
+import org.springframework.ai.content.Media;
 import org.springframework.ai.document.Document;
 import org.springframework.ai.embedding.Embedding;
 import org.springframework.ai.embedding.EmbeddingModel;
 import org.springframework.ai.embedding.EmbeddingRequest;
 import org.springframework.ai.embedding.EmbeddingResponse;
 import org.springframework.ai.embedding.EmbeddingResponseMetadata;
+import org.springframework.util.MimeTypeUtils;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -140,6 +149,24 @@ public class SpringAiEmbeddingsComponentTest extends 
CamelTestSupport {
         assertThat(vectors.get(0)).hasSameSizeAs(vectors.get(2));
     }
 
+    @Test
+    public void testSimpleListFunctionGivesBatch() throws Exception {
+        // the batch example of the component doc (XML and YAML)
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:batch")
+                        .setBody(simple("${list(text 1,text 2,text 3)}"))
+                        .to("spring-ai-embeddings:myEmbedding");
+            }
+        });
+
+        Message result = 
fluentTemplate.to("direct:batch").request(Message.class);
+
+        assertThat(requests).containsExactly(List.of("text 1", "text 2", "text 
3"));
+        assertThat(result.getBody(List.class)).hasSize(3);
+    }
+
     @Test
     public void testListOfOneTextGivesList() {
         Message result = fluentTemplate.to("spring-ai-embeddings:one")
@@ -157,15 +184,145 @@ public class SpringAiEmbeddingsComponentTest extends 
CamelTestSupport {
     }
 
     @Test
-    public void testListOfNonStringsIsConvertedToText() {
+    public void testListOfTextCarriersIsConvertedToText() {
         Message result = fluentTemplate.to("spring-ai-embeddings:convert")
-                .withBody(List.of(42, new StringBuilder("hello")))
+                .withBody(List.of(new StringBuilder("hello"), 
"bytes".getBytes(StandardCharsets.UTF_8),
+                        new Document("document text")))
                 .request(Message.class);
 
-        assertThat(requests).containsExactly(List.of("42", "hello"));
+        assertThat(requests).containsExactly(List.of("hello", "bytes", 
"document text"));
         List<float[]> vectors = result.getBody(List.class);
-        assertThat(vectors).hasSize(2);
-        assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXTS, 
List.class)).containsExactly("42", "hello");
+        assertThat(vectors).hasSize(3);
+        assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXTS, 
List.class))
+                .containsExactly("hello", "bytes", "document text");
+    }
+
+    @Test
+    public void testDocumentBodyEmbedsItsText() {
+        Message result = fluentTemplate.to("spring-ai-embeddings:document")
+                .withBody(new Document("document text", Map.of("source", 
"test")))
+                .request(Message.class);
+
+        assertThat(requests).containsExactly(List.of("document text"));
+        assertThat(result.getBody(float[].class)).containsExactly(0.1f, 0.2f, 
0.3f, 0.4f, 0.5f);
+        
assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXT)).isEqualTo("document
 text");
+    }
+
+    @Test
+    public void testDocumentWithoutTextFails() {
+        Document media = new Document(
+                new Media(MimeTypeUtils.IMAGE_PNG, 
URI.create("https://example.com/image.png";)),
+                Map.of());
+        Exchange exchange = template.send("spring-ai-embeddings:media", e -> 
e.getMessage().setBody(media));
+
+        assertThat(exchange.getException())
+                .isInstanceOf(InvalidPayloadException.class)
+                .hasMessageContaining("the Document has no text");
+        assertThat(requests).isEmpty();
+    }
+
+    @Test
+    public void testStreamBodyIsReadAsText() {
+        Message result = fluentTemplate.to("spring-ai-embeddings:stream")
+                .withBody(new ByteArrayInputStream("streamed 
text".getBytes(StandardCharsets.UTF_8)))
+                .request(Message.class);
+
+        assertThat(requests).containsExactly(List.of("streamed text"));
+        
assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXT)).isEqualTo("streamed
 text");
+    }
+
+    @Test
+    public void testMapBodyFails() {
+        Exchange exchange = template.send("spring-ai-embeddings:map",
+                e -> e.getMessage().setBody(Map.of("a", 1)));
+
+        assertThat(exchange.getException())
+                .isInstanceOf(InvalidPayloadException.class)
+                .hasMessageContaining("convert the body to text before the 
endpoint");
+        assertThat(requests).isEmpty();
+    }
+
+    @Test
+    public void testPojoBodyFails() {
+        Exchange exchange = template.send("spring-ai-embeddings:pojo",
+                e -> e.getMessage().setBody(new Pojo("hello")));
+
+        assertThat(exchange.getException())
+                .isInstanceOf(InvalidPayloadException.class)
+                .hasMessageContaining("convert the body to text before the 
endpoint");
+        assertThat(requests).isEmpty();
+    }
+
+    @Test
+    public void testListWithMapElementFails() {
+        Exchange exchange = template.send("spring-ai-embeddings:listmap",
+                e -> e.getMessage().setBody(List.of("hello", Map.of("a", 1))));
+
+        assertThat(exchange.getException())
+                .isInstanceOf(InvalidPayloadException.class)
+                .hasMessageContaining("the element at index 1 of the List body 
is java.util.")
+                .hasMessageContaining("convert the body to text before the 
endpoint");
+        assertThat(requests).isEmpty();
+    }
+
+    @Test
+    public void testListWithNumberElementFails() {
+        Exchange exchange = template.send("spring-ai-embeddings:listnumber",
+                e -> e.getMessage().setBody(List.of("hello", 42)));
+
+        assertThat(exchange.getException())
+                .isInstanceOf(InvalidPayloadException.class)
+                .hasMessageContaining("the element at index 1 of the List body 
is java.lang.Integer");
+        assertThat(requests).isEmpty();
+    }
+
+    @Test
+    public void testCharArrayBodyFailsAndConvertBodyToStringEmbedsIt() throws 
Exception {
+        // the upgrade guide: a body Camel converts to text but that is not 
text, bytes, a stream or a file, such as a
+        // char[], fails; convertBodyTo(String.class) before the endpoint 
embeds its text
+        Exchange exchange = template.send("spring-ai-embeddings:chars",
+                e -> e.getMessage().setBody("hello".toCharArray()));
+
+        assertThat(exchange.getException())
+                .isInstanceOf(InvalidPayloadException.class)
+                .hasMessageContaining("convert the body to text before the 
endpoint");
+        assertThat(requests).isEmpty();
+
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:chars")
+                        .convertBodyTo(String.class)
+                        .to("spring-ai-embeddings:chars");
+            }
+        });
+
+        Message result = 
fluentTemplate.to("direct:chars").withBody("hello".toCharArray()).request(Message.class);
+
+        assertThat(requests).containsExactly(List.of("hello"));
+        
assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXT)).isEqualTo("hello");
+    }
+
+    @Test
+    public void testSetBodyIsNotBatchedAndFails() {
+        Exchange exchange = template.send("spring-ai-embeddings:set",
+                e -> e.getMessage().setBody(new LinkedHashSet<>(List.of("a", 
"b"))));
+
+        assertThat(exchange.getException())
+                .isInstanceOf(InvalidPayloadException.class)
+                .hasMessageContaining("convert the body to text before the 
endpoint");
+        assertThat(requests).isEmpty();
+    }
+
+    @Test
+    public void testStringArrayBodyIsNotBatchedAndFails() {
+        Exchange exchange = template.send("spring-ai-embeddings:array",
+                e -> e.getMessage().setBody(new String[] { "a", "b" }));
+
+        assertThat(exchange.getException())
+                .isInstanceOf(InvalidPayloadException.class)
+                .hasMessageContaining("convert the body to text before the 
endpoint");
+        assertThat(requests).isEmpty();
     }
 
     @Test
@@ -191,4 +348,17 @@ public class SpringAiEmbeddingsComponentTest extends 
CamelTestSupport {
         assertThat(result.getHeader(SpringAiEmbeddingsHeaders.INPUT_TEXTS, 
List.class)).isEmpty();
         assertThat(requests).isEmpty();
     }
+
+    private static final class Pojo {
+        private final String text;
+
+        Pojo(String text) {
+            this.text = text;
+        }
+
+        @Override
+        public String toString() {
+            return "Pojo[text=" + text + "]";
+        }
+    }
 }
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index d6da752a051d..22b29bdfcb8c 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -1120,16 +1120,27 @@ stream failed after part of the request had already 
been sent.
 The JSON of the event is now written before the request is sent, with a 
`Content-Length` instead of a chunked request
 body. A body or a header that cannot be written as JSON fails the exchange 
without sending anything to Splunk.
 
-=== camel-spring-ai-embeddings - a List body always gives a List of vectors
+=== camel-spring-ai-embeddings - a List body always gives a List of vectors, 
and a Map or POJO body fails
 
 A `List` body with one text now gives a `List<float[]>` with one vector, as a 
`List` of more texts does, and sets the
 `CamelSpringAiEmbeddings` and `CamelSpringAiEmbeddingInputTexts` headers. 
Before, it gave a single `float[]` and set the
 `CamelSpringAiEmbedding`, `CamelSpringAiEmbeddingIndex` and 
`CamelSpringAiEmbeddingInputText` headers, as for a `String`
 body. A `String` body still gives a single `float[]`.
 
-Each element of a `List` body is now converted to a `String`, and a `null` 
element fails with an
-`IllegalArgumentException`. Before, a `List` of other objects failed with a 
`ClassCastException` in the embedding
-model. An empty `List` now gives an empty `List` without calling the embedding 
model.
+Each element of a `List` body is now read as text, and a `null` element fails 
with an `IllegalArgumentException`.
+Before, a `List` of other objects failed with a `ClassCastException` in the 
embedding model. An empty `List` now gives
+an empty `List` without calling the embedding model.
+
+The body, and each element of a `List` body, must now be text, bytes, a 
stream, a `Reader`, a file or a Spring AI
+`Document` (its text is embedded). Any other value, such as a `Map`, a POJO or 
a number, fails the exchange with an
+`InvalidPayloadException`. Before, such a body was embedded as its 
`toString()`, for example `{a=1, b=2}`, and a
+`Document` too. Convert the body to text before the endpoint, with 
`convertBodyTo(String.class)` or a marshal to JSON.
+
+This also applies to bodies that Camel can convert to text but that are not in 
the list above, and that were embedded
+as their text before: a `char[]`, a Netty `ByteBuf` (for example from 
netty-http with `disableStreamCache=true`), and a
+custom type with its own type converter to `String`. A `List` with such an 
element fails too, as do a `Set` and an
+array of texts (only a `java.util.List` is embedded as a batch). Add 
`convertBodyTo(String.class)` before the
+endpoint for such a body, or convert each element of the `List` to a `String`.
 
 === camel-spring-ai-vector-store - DELETE takes the ids from the body first, 
and the embeddings headers go with a vector body
 

Reply via email to