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 44791372edc9 CAMEL-26175: camel-odata - send JSON given as bytes, a 
stream or a file as it is, fail an empty CREATE/UPDATE body, and read a 
response that is not a JSON object (#29003)
44791372edc9 is described below

commit 44791372edc9dabefa71dd62f95715603a6630d6
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 3 07:38:40 2026 +0100

    CAMEL-26175: camel-odata - send JSON given as bytes, a stream or a file as 
it is, fail an empty CREATE/UPDATE body, and read a response that is not a JSON 
object (#29003)
    
    …le as it is, fail an empty CREATE/UPDATE body, and read a response that is 
not a JSON object
    
    CREATE and UPDATE passed every body that is not a String to 
Jsoner.serialize, so a byte
    array was sent as a JSON array of numbers and a stream or a file failed. 
They are now read
    as text and sent as they are. A null or empty body sent a POST or PATCH 
with no content;
    it now fails with a clear message. A JSON array or plain value response 
failed with
    "Expected an OData JSON object response"; it is now the parsed body. The 
operation option
    shows its READ_SET default in the catalog.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/catalog/components/odata.json |   6 +-
 .../apache/camel/catalog/docs/odata-component.adoc |  15 ++-
 .../org/apache/camel/component/odata/odata.json    |   6 +-
 .../camel-odata/src/main/docs/odata-component.adoc |  15 ++-
 .../camel/component/odata/ODataConfiguration.java  |   2 +-
 .../camel/component/odata/ODataEndpoint.java       |  25 +++--
 .../apache/camel/component/odata/ODataHelper.java  |  33 ++++--
 .../camel/component/odata/ODataProducer.java       |  54 ++++++---
 .../camel/component/odata/ODataProducerTest.java   | 125 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  11 ++
 .../endpoint/dsl/ODataEndpointBuilderFactory.java  |   2 +
 11 files changed, 250 insertions(+), 44 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/odata.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/odata.json
index bad2888ec647..a1776b176139 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/odata.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/odata.json
@@ -42,15 +42,15 @@
     "CamelODataSkip": { "index": 11, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "Integer", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The OData skip query option", "constantName": 
"org.apache.camel.component.odata.ODataConstants#SKIP" }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": 
"operation", "value": "CREATE", "carriers": [ "text", "map", "list" ] }, { 
"option": "operation", "value": "UPDATE", "carriers": [ "text", "map", "list" ] 
} ], "description": "The body is not read by READ_SET, READ_ENTRY and DELETE. 
CREATE and UPDATE send a String body as the JSON payload as it is, and 
serialize a Map or a List to JSON; a stream, a file or a POJO fails, and bytes 
are sent as a JSON array of numbers: con [...]
-    "producerOut": { "carriers": [ "map", "null" ], "description": "The JSON 
object of the response parsed into a Map (a JsonObject); for READ_SET the 
entities are in its value entry. No body for a 204 or an empty response, such 
as of a DELETE. The response code, the ETag, the count and the next link are in 
headers" }
+    "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": 
"operation", "value": "CREATE", "carriers": [ "text", "bytes", "stream", 
"file", "map", "list" ] }, { "option": "operation", "value": "UPDATE", 
"carriers": [ "text", "bytes", "stream", "file", "map", "list" ] } ], 
"description": "The body is not read by READ_SET, READ_ENTRY and DELETE. CREATE 
and UPDATE send JSON text as a String, bytes, a stream or a file as the JSON 
payload as it is, and serialize a Map or a List to J [...]
+    "producerOut": { "carriers": [ "map", "list", "value", "text", "null" ], 
"description": "The JSON object of the response parsed into a Map (a 
JsonObject); for READ_SET the entities are in its value entry. A JSON array is 
parsed into a List (a JsonArray), and a plain value into a number, a String or 
a boolean. No body for a 204 or an empty response, such as of a DELETE. The 
response code, the ETag, the count and the next link are in headers" }
   },
   "properties": {
     "httpUri": { "index": 0, "kind": "path", "displayName": "Http Uri", 
"group": "producer", "label": "", "required": true, "type": "string", 
"javaType": "java.net.URI", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "The base OData service 
URI" },
     "count": { "index": 1, "kind": "parameter", "displayName": "Count", 
"group": "producer", "label": "producer", "required": false, "type": "boolean", 
"javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "description": "The OData $count query parameter" },
     "expand": { "index": 2, "kind": "parameter", "displayName": "Expand", 
"group": "producer", "label": "producer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "description": "The OData $expand query parameter" },
     "filter": { "index": 3, "kind": "parameter", "displayName": "Filter", 
"group": "producer", "label": "producer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "description": "The OData $filter query parameter" },
-    "operation": { "index": 4, "kind": "parameter", "displayName": 
"Operation", "group": "producer", "label": "producer", "required": false, 
"type": "enum", "javaType": "org.apache.camel.component.odata.ODataOperation", 
"enum": [ "READ_SET", "READ_ENTRY", "CREATE", "UPDATE", "DELETE" ], 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "description": "The OData oper [...]
+    "operation": { "index": 4, "kind": "parameter", "displayName": 
"Operation", "group": "producer", "label": "producer", "required": false, 
"type": "enum", "javaType": "org.apache.camel.component.odata.ODataOperation", 
"enum": [ "READ_SET", "READ_ENTRY", "CREATE", "UPDATE", "DELETE" ], 
"deprecated": false, "autowired": false, "secret": false, "defaultValue": 
"READ_SET", "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "d [...]
     "orderBy": { "index": 5, "kind": "parameter", "displayName": "Order By", 
"group": "producer", "label": "producer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "description": "The OData $orderby query parameter" },
     "select": { "index": 6, "kind": "parameter", "displayName": "Select", 
"group": "producer", "label": "producer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "description": "The OData $select query parameter" },
     "skip": { "index": 7, "kind": "parameter", "displayName": "Skip", "group": 
"producer", "label": "producer", "required": false, "type": "integer", 
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "description": "The OData $skip query parameter" },
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/odata-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/odata-component.adoc
index 3a11d856ebe7..657e5e417193 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/odata-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/odata-component.adoc
@@ -122,6 +122,18 @@ For example, a string key can be supplied as `'ABC-123'`, 
while a composite key
 
 The key value should be a valid and trusted OData key expression.
 
+=== Message Body
+
+`CREATE` and `UPDATE` send the JSON of the entity from the message body:
+
+* JSON text as a `String`, a byte array, a stream or a file is sent as it is.
+* A `Map` or a `List` is serialized to JSON.
+
+Other Java objects (POJOs) are not serialized: marshal them to JSON first, for 
example with the JSON data format.
+An empty body fails, as these operations need an entity: set the body to `{}` 
to send an empty JSON object.
+
+The other operations do not read the message body.
+
 == Message Headers
 
 The following message headers can be used to control OData processing 
dynamically.
@@ -447,7 +459,8 @@ exchange.getMessage().getHeader("CamelODataCount");
 exchange.getMessage().getHeader("CamelODataNextLink");
 ----
 
-The response body contains the parsed JSON response.
+The response body contains the parsed JSON response: a JSON object is a `Map`, 
a JSON array a `List`,
+and a plain value a number, a `String` or a boolean. A `204` or an empty 
response gives no body.
 
 == SSL Configuration
 
diff --git 
a/components/camel-odata/src/generated/resources/META-INF/org/apache/camel/component/odata/odata.json
 
b/components/camel-odata/src/generated/resources/META-INF/org/apache/camel/component/odata/odata.json
index bad2888ec647..a1776b176139 100644
--- 
a/components/camel-odata/src/generated/resources/META-INF/org/apache/camel/component/odata/odata.json
+++ 
b/components/camel-odata/src/generated/resources/META-INF/org/apache/camel/component/odata/odata.json
@@ -42,15 +42,15 @@
     "CamelODataSkip": { "index": 11, "kind": "header", "displayName": "", 
"group": "producer", "label": "", "required": false, "javaType": "Integer", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The OData skip query option", "constantName": 
"org.apache.camel.component.odata.ODataConstants#SKIP" }
   },
   "bodyTypes": {
-    "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": 
"operation", "value": "CREATE", "carriers": [ "text", "map", "list" ] }, { 
"option": "operation", "value": "UPDATE", "carriers": [ "text", "map", "list" ] 
} ], "description": "The body is not read by READ_SET, READ_ENTRY and DELETE. 
CREATE and UPDATE send a String body as the JSON payload as it is, and 
serialize a Map or a List to JSON; a stream, a file or a POJO fails, and bytes 
are sent as a JSON array of numbers: con [...]
-    "producerOut": { "carriers": [ "map", "null" ], "description": "The JSON 
object of the response parsed into a Map (a JsonObject); for READ_SET the 
entities are in its value entry. No body for a 204 or an empty response, such 
as of a DELETE. The response code, the ETag, the count and the next link are in 
headers" }
+    "producerIn": { "carriers": [ "any" ], "byOption": [ { "option": 
"operation", "value": "CREATE", "carriers": [ "text", "bytes", "stream", 
"file", "map", "list" ] }, { "option": "operation", "value": "UPDATE", 
"carriers": [ "text", "bytes", "stream", "file", "map", "list" ] } ], 
"description": "The body is not read by READ_SET, READ_ENTRY and DELETE. CREATE 
and UPDATE send JSON text as a String, bytes, a stream or a file as the JSON 
payload as it is, and serialize a Map or a List to J [...]
+    "producerOut": { "carriers": [ "map", "list", "value", "text", "null" ], 
"description": "The JSON object of the response parsed into a Map (a 
JsonObject); for READ_SET the entities are in its value entry. A JSON array is 
parsed into a List (a JsonArray), and a plain value into a number, a String or 
a boolean. No body for a 204 or an empty response, such as of a DELETE. The 
response code, the ETag, the count and the next link are in headers" }
   },
   "properties": {
     "httpUri": { "index": 0, "kind": "path", "displayName": "Http Uri", 
"group": "producer", "label": "", "required": true, "type": "string", 
"javaType": "java.net.URI", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "The base OData service 
URI" },
     "count": { "index": 1, "kind": "parameter", "displayName": "Count", 
"group": "producer", "label": "producer", "required": false, "type": "boolean", 
"javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "description": "The OData $count query parameter" },
     "expand": { "index": 2, "kind": "parameter", "displayName": "Expand", 
"group": "producer", "label": "producer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "description": "The OData $expand query parameter" },
     "filter": { "index": 3, "kind": "parameter", "displayName": "Filter", 
"group": "producer", "label": "producer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "description": "The OData $filter query parameter" },
-    "operation": { "index": 4, "kind": "parameter", "displayName": 
"Operation", "group": "producer", "label": "producer", "required": false, 
"type": "enum", "javaType": "org.apache.camel.component.odata.ODataOperation", 
"enum": [ "READ_SET", "READ_ENTRY", "CREATE", "UPDATE", "DELETE" ], 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "description": "The OData oper [...]
+    "operation": { "index": 4, "kind": "parameter", "displayName": 
"Operation", "group": "producer", "label": "producer", "required": false, 
"type": "enum", "javaType": "org.apache.camel.component.odata.ODataOperation", 
"enum": [ "READ_SET", "READ_ENTRY", "CREATE", "UPDATE", "DELETE" ], 
"deprecated": false, "autowired": false, "secret": false, "defaultValue": 
"READ_SET", "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "d [...]
     "orderBy": { "index": 5, "kind": "parameter", "displayName": "Order By", 
"group": "producer", "label": "producer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "description": "The OData $orderby query parameter" },
     "select": { "index": 6, "kind": "parameter", "displayName": "Select", 
"group": "producer", "label": "producer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "description": "The OData $select query parameter" },
     "skip": { "index": 7, "kind": "parameter", "displayName": "Skip", "group": 
"producer", "label": "producer", "required": false, "type": "integer", 
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.odata.ODataConfiguration", "configurationField": 
"configuration", "description": "The OData $skip query parameter" },
diff --git a/components/camel-odata/src/main/docs/odata-component.adoc 
b/components/camel-odata/src/main/docs/odata-component.adoc
index 3a11d856ebe7..657e5e417193 100644
--- a/components/camel-odata/src/main/docs/odata-component.adoc
+++ b/components/camel-odata/src/main/docs/odata-component.adoc
@@ -122,6 +122,18 @@ For example, a string key can be supplied as `'ABC-123'`, 
while a composite key
 
 The key value should be a valid and trusted OData key expression.
 
+=== Message Body
+
+`CREATE` and `UPDATE` send the JSON of the entity from the message body:
+
+* JSON text as a `String`, a byte array, a stream or a file is sent as it is.
+* A `Map` or a `List` is serialized to JSON.
+
+Other Java objects (POJOs) are not serialized: marshal them to JSON first, for 
example with the JSON data format.
+An empty body fails, as these operations need an entity: set the body to `{}` 
to send an empty JSON object.
+
+The other operations do not read the message body.
+
 == Message Headers
 
 The following message headers can be used to control OData processing 
dynamically.
@@ -447,7 +459,8 @@ exchange.getMessage().getHeader("CamelODataCount");
 exchange.getMessage().getHeader("CamelODataNextLink");
 ----
 
-The response body contains the parsed JSON response.
+The response body contains the parsed JSON response: a JSON object is a `Map`, 
a JSON array a `List`,
+and a plain value a number, a `String` or a boolean. A `204` or an empty 
response gives no body.
 
 == SSL Configuration
 
diff --git 
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java
 
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java
index 807e5357e39c..0a60e60ed34c 100644
--- 
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java
+++ 
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java
@@ -23,7 +23,7 @@ import org.apache.camel.support.jsse.SSLContextParameters;
 @UriParams
 public class ODataConfiguration {
 
-    @UriParam(label = "producer", description = "The OData operation to 
perform")
+    @UriParam(label = "producer", defaultValue = "READ_SET", description = 
"The OData operation to perform")
     private ODataOperation operation = ODataOperation.READ_SET;
 
     @UriParam(label = "producer", description = "The OData $filter query 
parameter")
diff --git 
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataEndpoint.java
 
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataEndpoint.java
index 6259aae79fda..889319966003 100644
--- 
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataEndpoint.java
+++ 
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataEndpoint.java
@@ -50,19 +50,24 @@ import org.apache.camel.support.jsse.SSLContextParameters;
 @BodyTypes(producerIn = @BodyType(value = Carrier.ANY,
                                   byOption = {
                                           @BodyType.When(option = "operation", 
value = "CREATE",
-                                                         carriers = { 
Carrier.TEXT, Carrier.MAP, Carrier.LIST }),
+                                                         carriers = {
+                                                                 Carrier.TEXT, 
Carrier.BYTES, Carrier.STREAM, Carrier.FILE,
+                                                                 Carrier.MAP, 
Carrier.LIST }),
                                           @BodyType.When(option = "operation", 
value = "UPDATE",
-                                                         carriers = { 
Carrier.TEXT, Carrier.MAP, Carrier.LIST }) },
+                                                         carriers = {
+                                                                 Carrier.TEXT, 
Carrier.BYTES, Carrier.STREAM, Carrier.FILE,
+                                                                 Carrier.MAP, 
Carrier.LIST }) },
                                   description = "The body is not read by 
READ_SET, READ_ENTRY and DELETE. CREATE and UPDATE"
-                                                + " send a String body as the 
JSON payload as it is, and serialize a Map or a"
-                                                + " List to JSON; a stream, a 
file or a POJO fails, and bytes are sent as a"
-                                                + " JSON array of numbers: 
convert them to a String first. The operation can"
-                                                + " also be set with the 
CamelODataOperation header"),
-           producerOut = @BodyType(value = { Carrier.MAP, Carrier.NULL },
+                                                + " send JSON text as a 
String, bytes, a stream or a file as the JSON payload"
+                                                + " as it is, and serialize a 
Map or a List to JSON; a POJO fails, and so does"
+                                                + " an empty body (use {} for 
an empty JSON object). The operation can also be"
+                                                + " set with the 
CamelODataOperation header"),
+           producerOut = @BodyType(value = { Carrier.MAP, Carrier.LIST, 
Carrier.VALUE, Carrier.TEXT, Carrier.NULL },
                                    description = "The JSON object of the 
response parsed into a Map (a JsonObject); for"
-                                                 + " READ_SET the entities are 
in its value entry. No body for a 204 or an"
-                                                 + " empty response, such as 
of a DELETE. The response code, the ETag, the"
-                                                 + " count and the next link 
are in headers"))
+                                                 + " READ_SET the entities are 
in its value entry. A JSON array is parsed"
+                                                 + " into a List (a 
JsonArray), and a plain value into a number, a String or a"
+                                                 + " boolean. No body for a 
204 or an empty response, such as of a DELETE."
+                                                 + " The response code, the 
ETag, the count and the next link are in headers"))
 public class ODataEndpoint extends DefaultEndpoint implements 
EndpointServiceLocation, SSLContextParametersAware {
 
     @UriPath
diff --git 
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataHelper.java
 
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataHelper.java
index 1376e7a66a22..9b5e4105fb7b 100644
--- 
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataHelper.java
+++ 
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataHelper.java
@@ -16,8 +16,13 @@
  */
 package org.apache.camel.component.odata;
 
-import java.util.Map;
+import java.io.File;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.file.Path;
 
+import org.apache.camel.StreamCache;
+import org.apache.camel.WrappedFile;
 import org.apache.camel.util.json.DeserializationException;
 import org.apache.camel.util.json.Jsoner;
 
@@ -38,17 +43,25 @@ public final class ODataHelper {
         return Jsoner.serialize(value);
     }
 
-    @SuppressWarnings("unchecked")
-    public static Map<String, Object> parseJsonObject(String value) throws 
DeserializationException {
-        if (value == null || value.isBlank()) {
-            return Map.of();
-        }
+    /**
+     * Whether the body is a payload that holds the JSON text as it is: bytes, 
a stream or a file. It is read as text
+     * and sent as it is, as a String body is, and not serialized to JSON.
+     */
+    public static boolean isPayload(Object value) {
+        return value instanceof byte[]
+                || value instanceof InputStream || value instanceof Reader || 
value instanceof StreamCache
+                || value instanceof File || value instanceof Path || value 
instanceof WrappedFile;
+    }
 
-        Object parsed = Jsoner.deserialize(value);
-        if (!(parsed instanceof Map)) {
-            throw new IllegalArgumentException("Expected an OData JSON object 
response");
+    /**
+     * Parses the JSON of a response: a JSON object is a Map, a JSON array a 
List, and a plain value a number, a String
+     * or a boolean.
+     */
+    public static Object parseJson(String value) throws 
DeserializationException {
+        if (value == null || value.isBlank()) {
+            return null;
         }
 
-        return (Map<String, Object>) parsed;
+        return Jsoner.deserialize(value);
     }
 }
diff --git 
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataProducer.java
 
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataProducer.java
index fb717a00242c..61294bc73b12 100644
--- 
a/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataProducer.java
+++ 
b/components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataProducer.java
@@ -25,6 +25,7 @@ import java.util.Map;
 import org.apache.camel.Component;
 import org.apache.camel.Endpoint;
 import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
 import org.apache.camel.Producer;
 import org.apache.camel.component.http.HttpConstants;
 import org.apache.camel.support.DefaultProducer;
@@ -151,12 +152,12 @@ public class ODataProducer extends DefaultProducer {
             case CREATE -> {
                 httpExchange.getMessage().setHeader(HttpConstants.HTTP_METHOD, 
"POST");
                 httpExchange.getMessage().setHeader(Exchange.CONTENT_TYPE, 
"application/json");
-                
httpExchange.getMessage().setBody(ODataHelper.toJson(exchange.getMessage().getBody()));
+                httpExchange.getMessage().setBody(toJsonPayload(exchange, 
operation));
             }
             case UPDATE -> {
                 httpExchange.getMessage().setHeader(HttpConstants.HTTP_METHOD, 
"PATCH");
                 httpExchange.getMessage().setHeader(Exchange.CONTENT_TYPE, 
"application/json");
-                
httpExchange.getMessage().setBody(ODataHelper.toJson(exchange.getMessage().getBody()));
+                httpExchange.getMessage().setBody(toJsonPayload(exchange, 
operation));
             }
             case DELETE -> {
                 httpExchange.getMessage().setHeader(HttpConstants.HTTP_METHOD, 
"DELETE");
@@ -200,22 +201,25 @@ public class ODataProducer extends DefaultProducer {
             return;
         }
 
-        Map<String, Object> response = ODataHelper.parseJsonObject(body);
+        Object response = ODataHelper.parseJson(body);
 
-        Object count = response.get("@odata.count");
-        if (count instanceof Number number) {
-            exchange.getMessage().setHeader(ODataConstants.COUNT, 
number.longValue());
-        }
+        // a JSON array or a plain value is the body as it is: only a JSON 
object has the OData annotations
+        if (response instanceof Map<?, ?> map) {
+            Object count = map.get("@odata.count");
+            if (count instanceof Number number) {
+                exchange.getMessage().setHeader(ODataConstants.COUNT, 
number.longValue());
+            }
 
-        Object nextLink = response.get("@odata.nextLink");
-        if (nextLink != null) {
-            exchange.getMessage().setHeader(ODataConstants.NEXT_LINK, 
nextLink.toString());
-        }
+            Object nextLink = map.get("@odata.nextLink");
+            if (nextLink != null) {
+                exchange.getMessage().setHeader(ODataConstants.NEXT_LINK, 
nextLink.toString());
+            }
 
-        if (responseEtag == null) {
-            Object odataEtag = response.get("@odata.etag");
-            if (odataEtag != null) {
-                responseEtag = odataEtag.toString();
+            if (responseEtag == null) {
+                Object odataEtag = map.get("@odata.etag");
+                if (odataEtag != null) {
+                    responseEtag = odataEtag.toString();
+                }
             }
         }
 
@@ -226,6 +230,26 @@ public class ODataProducer extends DefaultProducer {
         exchange.getMessage().setBody(response);
     }
 
+    /**
+     * The JSON of the entity that CREATE and UPDATE send: JSON text as bytes, 
a stream or a file is sent as it is, as a
+     * String is, and a Map or a List is serialized to JSON. These operations 
need an entity, so an empty body fails.
+     */
+    private static String toJsonPayload(Exchange exchange, ODataOperation 
operation) throws InvalidPayloadException {
+        Object body = exchange.getMessage().getBody();
+        String json;
+        if (ODataHelper.isPayload(body)) {
+            json = exchange.getMessage().getMandatoryBody(String.class);
+        } else {
+            json = ODataHelper.toJson(body);
+        }
+        if (json == null || json.isBlank()) {
+            throw new IllegalArgumentException(
+                    "The " + operation + " operation needs the JSON of the 
entity as the body, but the body is empty:"
+                                               + " set the body to {} to send 
an empty JSON object");
+        }
+        return json;
+    }
+
     private String buildTargetUri(Exchange exchange, ODataOperation operation) 
throws Exception {
         String base = endpoint.getHttpUri().toString();
 
diff --git 
a/components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java
 
b/components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java
index 855c05e1836a..94102786e804 100644
--- 
a/components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java
+++ 
b/components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java
@@ -16,12 +16,17 @@
  */
 package org.apache.camel.component.odata;
 
+import java.io.ByteArrayInputStream;
+import java.io.File;
 import java.math.BigDecimal;
 import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Base64;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.ConcurrentLinkedQueue;
@@ -45,6 +50,7 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.io.TempDir;
 
 import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
 import static com.github.tomakehurst.wiremock.client.WireMock.containing;
@@ -57,9 +63,11 @@ import static 
com.github.tomakehurst.wiremock.client.WireMock.matching;
 import static com.github.tomakehurst.wiremock.client.WireMock.patch;
 import static 
com.github.tomakehurst.wiremock.client.WireMock.patchRequestedFor;
 import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
 import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
 import static 
com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -1143,4 +1151,121 @@ public class ODataProducerTest extends CamelTestSupport 
{
         wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
                 .withHeader("Authorization", equalTo(expected2)));
     }
+
+    @Test
+    void testProcessCreateEntityWithBytesBody() throws Exception {
+        wireMock.stubFor(post(urlPathEqualTo("/odata/Products"))
+                
.withRequestBody(equalToJson("{\"Name\":\"Tablet\",\"Price\":299.99}"))
+                .willReturn(aResponse()
+                        .withStatus(201)
+                        .withHeader("Content-Type", "application/json")
+                        .withBody("{\"ID\":103,\"Name\":\"Tablet\"}")));
+
+        Exchange exchange = endpoint.createExchange();
+        exchange.getMessage().setHeader(ODataConstants.OPERATION, 
ODataOperation.CREATE);
+        
exchange.getMessage().setBody("{\"Name\":\"Tablet\",\"Price\":299.99}".getBytes(StandardCharsets.UTF_8));
+
+        producer.process(exchange);
+
+        assertEquals(201, 
exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+        wireMock.verify(postRequestedFor(urlPathEqualTo("/odata/Products"))
+                .withHeader("Content-Type", containing("application/json"))
+                
.withRequestBody(equalToJson("{\"Name\":\"Tablet\",\"Price\":299.99}")));
+    }
+
+    @Test
+    void testProcessCreateEntityWithStreamBody() throws Exception {
+        wireMock.stubFor(post(urlPathEqualTo("/odata/Products"))
+                .withRequestBody(equalToJson("{\"Name\":\"Tablet\"}"))
+                .willReturn(aResponse()
+                        .withStatus(201)
+                        .withHeader("Content-Type", "application/json")
+                        .withBody("{\"ID\":103,\"Name\":\"Tablet\"}")));
+
+        Exchange exchange = endpoint.createExchange();
+        exchange.getMessage().setHeader(ODataConstants.OPERATION, 
ODataOperation.CREATE);
+        exchange.getMessage().setBody(
+                new 
ByteArrayInputStream("{\"Name\":\"Tablet\"}".getBytes(StandardCharsets.UTF_8)));
+
+        producer.process(exchange);
+
+        assertEquals(201, 
exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+        Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+        assertEquals("Tablet", body.get("Name"));
+    }
+
+    @Test
+    void testProcessUpdateEntityWithFileBody(@TempDir Path dir) throws 
Exception {
+        File file = dir.resolve("product.json").toFile();
+        Files.writeString(file.toPath(), "{\"Name\":\"Updated Laptop\"}");
+
+        wireMock.stubFor(patch(urlPathEqualTo("/odata/Products(101)"))
+                .withRequestBody(equalToJson("{\"Name\":\"Updated Laptop\"}"))
+                .willReturn(aResponse().withStatus(204)));
+
+        Exchange exchange = endpoint.createExchange();
+        exchange.getMessage().setHeader(ODataConstants.OPERATION, 
ODataOperation.UPDATE);
+        exchange.getMessage().setHeader(ODataConstants.KEY, "101");
+        exchange.getMessage().setBody(file);
+
+        producer.process(exchange);
+
+        assertEquals(204, 
exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+        
wireMock.verify(patchRequestedFor(urlPathEqualTo("/odata/Products(101)"))
+                .withRequestBody(equalToJson("{\"Name\":\"Updated 
Laptop\"}")));
+    }
+
+    @Test
+    void testProcessCreateEntityWithNullBodyFails() {
+        Exchange exchange = endpoint.createExchange();
+        exchange.getMessage().setHeader(ODataConstants.OPERATION, 
ODataOperation.CREATE);
+
+        IllegalArgumentException e = 
assertThrows(IllegalArgumentException.class, () -> producer.process(exchange));
+        assertTrue(e.getMessage().contains("CREATE operation needs the JSON of 
the entity"), e.getMessage());
+
+        wireMock.verify(0, 
postRequestedFor(urlPathEqualTo("/odata/Products")));
+    }
+
+    @Test
+    void testProcessUpdateEntityWithEmptyBodyFails() {
+        Exchange exchange = endpoint.createExchange();
+        exchange.getMessage().setHeader(ODataConstants.OPERATION, 
ODataOperation.UPDATE);
+        exchange.getMessage().setHeader(ODataConstants.KEY, "101");
+        exchange.getMessage().setBody(new byte[0]);
+
+        IllegalArgumentException e = 
assertThrows(IllegalArgumentException.class, () -> producer.process(exchange));
+        assertTrue(e.getMessage().contains("UPDATE operation needs the JSON of 
the entity"), e.getMessage());
+
+        wireMock.verify(0, 
patchRequestedFor(urlPathEqualTo("/odata/Products(101)")));
+    }
+
+    @Test
+    void testProcessJsonArrayResponse() throws Exception {
+        wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+                .willReturn(aResponse()
+                        .withStatus(200)
+                        .withHeader("Content-Type", "application/json")
+                        .withBody("[{\"ID\":101},{\"ID\":102}]")));
+
+        Exchange exchange = endpoint.createExchange();
+        producer.process(exchange);
+
+        List<?> body = assertInstanceOf(List.class, 
exchange.getMessage().getBody());
+        assertEquals(2, body.size());
+        assertNull(exchange.getMessage().getHeader(ODataConstants.COUNT));
+    }
+
+    @Test
+    void testProcessPlainValueResponse() throws Exception {
+        wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+                .willReturn(aResponse()
+                        .withStatus(200)
+                        .withHeader("Content-Type", "text/plain")
+                        .withBody("42")));
+
+        Exchange exchange = endpoint.createExchange();
+        producer.process(exchange);
+
+        assertEquals(new BigDecimal(42), exchange.getMessage().getBody());
+    }
 }
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 6e5da218ad82..958cee6549cd 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
@@ -1963,6 +1963,17 @@ When no HTTP response can be built from the reply of the 
route (for example the
 now answers with HTTP Status 500 and closes the connection. The body is empty, 
or the stack trace of the exception with
 `muteException=false`. Before, no response was sent and the client waited 
until it timed out.
 
+=== camel-odata - the body of CREATE and UPDATE, and a response that is not a 
JSON object
+
+`CREATE` and `UPDATE` send JSON text given as a byte array, a stream or a file 
as it is, as they do for a `String`.
+Before, a byte array was sent as a JSON array of numbers, and a stream or a 
file failed.
+
+`CREATE` and `UPDATE` now fail with an `IllegalArgumentException` when the 
body is null or empty. Before, they sent a
+`POST` or `PATCH` request with no content. Set the body to `{}` to send an 
empty JSON object.
+
+A response that is a JSON array or a plain value is now the body as it is 
parsed (a `List`, a number, a `String` or a
+boolean). Before, the producer failed with `Expected an OData JSON object 
response`.
+
 === camel-telegram - webhook requests are verified with a secret token
 
 In webhook mode, Camel now registers the webhook with a secret token, which 
Telegram sends in the
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/ODataEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/ODataEndpointBuilderFactory.java
index ff100ccf25b1..b2f7ad921456 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/ODataEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/ODataEndpointBuilderFactory.java
@@ -109,6 +109,7 @@ public interface ODataEndpointBuilderFactory {
          * The option is a:
          * <code>org.apache.camel.component.odata.ODataOperation</code> type.
          * 
+         * Default: READ_SET
          * Group: producer
          * 
          * @param operation the value to set
@@ -124,6 +125,7 @@ public interface ODataEndpointBuilderFactory {
          * The option will be converted to a
          * <code>org.apache.camel.component.odata.ODataOperation</code> type.
          * 
+         * Default: READ_SET
          * Group: producer
          * 
          * @param operation the value to set

Reply via email to