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 1085de3d8102 CAMEL-25329: JSON data formats - marshal json writes a 
body that is already JSON text as-is (#27350)
1085de3d8102 is described below

commit 1085de3d81023fac95258d26f3063ee75993275a
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 5 10:34:22 2026 +0200

    CAMEL-25329: JSON data formats - marshal json writes a body that is already 
JSON text as-is (#27350)
    
    * camel-jackson - marshal json writes a body that is already JSON text as-is
    
    marshal json treated every body as an object to serialize, so a body that
    already is the JSON text came out wrong: a String with a JSON object was
    double-encoded ("{\"sku\": ...}"), a byte[] became Base64, and an
    InputStream or a file failed with "No serializer found". A file, a stream
    or bytes are the serialized form of a payload, never an object, and a
    String holding a JSON object or array is the same text: the JSON
    JacksonDataFormat of camel-jackson and camel-jackson3 now writes them as
    they are. Any other String is still a JSON string, and POJOs, maps and lists
    are marshalled as before. No parsing: the type, and for a String its first
    and last character. Not in AbstractJacksonDataFormat, which the binary Avro
    format extends too. Upgrade guide note for 4.23.
    
    * all JSON data formats write a body that is already JSON text as-is
    
    The decision moves to JsonPayloadHelper in camel-support, so Gson, JSON-B 
and
    Fastjson do the same as Jackson and Jackson 3: a file, a stream, bytes, or a
    String holding a JSON object or array is written as it is; any other String
    and every object goes to the library as before. A file is read directly when
    it has one, without a type conversion. Fastjson sets Content-Length from the
    bytes written. The test of each module builds the file body directly instead
    of polling a directory, which once in a while gave the content twice in a
    full suite run. Docs of all five data formats and the upgrade note updated.
    
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../camel/catalog/docs/fastjson-dataformat.adoc    |  8 ++
 .../apache/camel/catalog/docs/gson-dataformat.adoc |  8 ++
 .../camel/catalog/docs/jackson2-dataformat.adoc    |  8 ++
 .../camel/catalog/docs/jackson3-dataformat.adoc    |  8 ++
 .../camel/catalog/docs/jsonb-dataformat.adoc       |  8 ++
 .../src/main/docs/fastjson-dataformat.adoc         |  8 ++
 .../component/fastjson/FastjsonDataFormat.java     | 11 +++
 .../fastjson/FastjsonMarshalJsonTextTest.java      | 97 ++++++++++++++++++++++
 .../camel-gson/src/main/docs/gson-dataformat.adoc  |  8 ++
 .../camel/component/gson/GsonDataFormat.java       |  8 ++
 .../component/gson/GsonMarshalJsonTextTest.java    | 97 ++++++++++++++++++++++
 .../src/main/docs/jackson2-dataformat.adoc         |  8 ++
 .../camel/component/jackson/JacksonDataFormat.java | 19 +++++
 .../jackson/JacksonMarshalJsonTextTest.java        | 97 ++++++++++++++++++++++
 .../src/main/docs/jackson3-dataformat.adoc         |  8 ++
 .../component/jackson3/JacksonDataFormat.java      | 19 +++++
 .../jackson3/JacksonMarshalJsonTextTest.java       | 97 ++++++++++++++++++++++
 .../src/main/docs/jsonb-dataformat.adoc            |  8 ++
 .../camel/component/jsonb/JsonbDataFormat.java     | 10 +++
 .../component/jsonb/JsonbMarshalJsonTextTest.java  | 97 ++++++++++++++++++++++
 .../camel/support/JsonPayloadHelperTest.java       | 65 +++++++++++++++
 .../apache/camel/support/JsonPayloadHelper.java    | 84 +++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    | 15 ++++
 23 files changed, 796 insertions(+)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/fastjson-dataformat.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/fastjson-dataformat.adoc
index f7c29f115c34..9e89cce982de 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/fastjson-dataformat.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/fastjson-dataformat.adoc
@@ -58,6 +58,14 @@ include::partial$dataformat-options.adoc[]
 // dataformat options: END
 
 
+== A body that is already JSON
+
+A body that already is the JSON text is written as it is, not serialized as a 
Java object: a file, an `InputStream`,
+a `byte[]`, and a `String` whose text is a JSON object or array (it starts 
with `{` and ends with `}`, or `[` and
+`]`). So `marshal: json` after reading a JSON file, or after building the JSON 
with a template or a Simple
+expression, sends that JSON unchanged. Any other `String` is written as a JSON 
string, and POJOs, maps and lists are
+marshalled as usual. All the JSON data formats of Camel do the same.
+
 == Dependencies
 
 To use Fastjson in your camel routes, you need to add the dependency
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/gson-dataformat.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/gson-dataformat.adoc
index a64e89343a85..07dfe44bf0de 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/gson-dataformat.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/gson-dataformat.adoc
@@ -58,6 +58,14 @@ include::partial$dataformat-options.adoc[]
 // dataformat options: END
 
 
+== A body that is already JSON
+
+A body that already is the JSON text is written as it is, not serialized as a 
Java object: a file, an `InputStream`,
+a `byte[]`, and a `String` whose text is a JSON object or array (it starts 
with `{` and ends with `}`, or `[` and
+`]`). So `marshal: json` after reading a JSON file, or after building the JSON 
with a template or a Simple
+expression, sends that JSON unchanged. Any other `String` is written as a JSON 
string, and POJOs, maps and lists are
+marshalled as usual. All the JSON data formats of Camel do the same.
+
 == Dependencies
 
 To use Gson in your camel routes, you need to add the dependency
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jackson2-dataformat.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jackson2-dataformat.adoc
index d1d8490a985d..96e49f1ee222 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jackson2-dataformat.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jackson2-dataformat.adoc
@@ -66,6 +66,14 @@ compliant with Json specification. This can however 
frustrate users, because it
 names and texts with Unicode escapes. To avoid this, users commonly use 
4-bytes would need to turn on `combineUnicodeSurrogates=true`
 in the Camel dataformat.
 
+=== A body that is already JSON
+
+A body that already is the JSON text is written as it is, not serialized as a 
Java object: a file, an `InputStream`,
+a `byte[]`, and a `String` whose text is a JSON object or array (it starts 
with `{` and ends with `}`, or `[` and
+`]`). So `marshal: json` after reading a JSON file, or after building the JSON 
with a template or a Simple
+expression, sends that JSON unchanged. Any other `String` is written as a JSON 
string, and POJOs, maps and lists are
+marshalled as usual. All the JSON data formats of Camel do the same.
+
 === Using custom ObjectMapper
 
 You can configure `JacksonDataFormat` to use a custom `ObjectMapper` in case 
you need more control of the mapping configuration.
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jackson3-dataformat.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jackson3-dataformat.adoc
index dc7f2530a71f..149c8ff60499 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jackson3-dataformat.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jackson3-dataformat.adoc
@@ -94,6 +94,14 @@ compliant with Json specification. This can however 
frustrate users, because it
 names and texts with Unicode escapes. To avoid this, users commonly use 
4-bytes would need to turn on `combineUnicodeSurrogates=true`
 in the Camel dataformat.
 
+=== A body that is already JSON
+
+A body that already is the JSON text is written as it is, not serialized as a 
Java object: a file, an `InputStream`,
+a `byte[]`, and a `String` whose text is a JSON object or array (it starts 
with `{` and ends with `}`, or `[` and
+`]`). So `marshal: json` after reading a JSON file, or after building the JSON 
with a template or a Simple
+expression, sends that JSON unchanged. Any other `String` is written as a JSON 
string, and POJOs, maps and lists are
+marshalled as usual. All the JSON data formats of Camel do the same.
+
 === Using custom ObjectMapper
 
 You can configure `JacksonDataFormat` to use a custom `ObjectMapper` in case 
you need more control of the mapping configuration.
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jsonb-dataformat.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jsonb-dataformat.adoc
index b948621aa613..3d0a251f888f 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jsonb-dataformat.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jsonb-dataformat.adoc
@@ -57,6 +57,14 @@ include::partial$dataformat-options.adoc[]
 // dataformat options: END
 
 
+== A body that is already JSON
+
+A body that already is the JSON text is written as it is, not serialized as a 
Java object: a file, an `InputStream`,
+a `byte[]`, and a `String` whose text is a JSON object or array (it starts 
with `{` and ends with `}`, or `[` and
+`]`). So `marshal: json` after reading a JSON file, or after building the JSON 
with a template or a Simple
+expression, sends that JSON unchanged. Any other `String` is written as a JSON 
string, and POJOs, maps and lists are
+marshalled as usual. All the JSON data formats of Camel do the same.
+
 == Dependencies
 
 To use JSON-B in your Camel routes, you need to add the dependency
diff --git a/components/camel-fastjson/src/main/docs/fastjson-dataformat.adoc 
b/components/camel-fastjson/src/main/docs/fastjson-dataformat.adoc
index f7c29f115c34..9e89cce982de 100644
--- a/components/camel-fastjson/src/main/docs/fastjson-dataformat.adoc
+++ b/components/camel-fastjson/src/main/docs/fastjson-dataformat.adoc
@@ -58,6 +58,14 @@ include::partial$dataformat-options.adoc[]
 // dataformat options: END
 
 
+== A body that is already JSON
+
+A body that already is the JSON text is written as it is, not serialized as a 
Java object: a file, an `InputStream`,
+a `byte[]`, and a `String` whose text is a JSON object or array (it starts 
with `{` and ends with `}`, or `[` and
+`]`). So `marshal: json` after reading a JSON file, or after building the JSON 
with a template or a Simple
+expression, sends that JSON unchanged. Any other `String` is written as a JSON 
string, and POJOs, maps and lists are
+marshalled as usual. All the JSON data formats of Camel do the same.
+
 == Dependencies
 
 To use Fastjson in your camel routes, you need to add the dependency
diff --git 
a/components/camel-fastjson/src/main/java/org/apache/camel/component/fastjson/FastjsonDataFormat.java
 
b/components/camel-fastjson/src/main/java/org/apache/camel/component/fastjson/FastjsonDataFormat.java
index 28d13d35793b..de6af31cee91 100644
--- 
a/components/camel-fastjson/src/main/java/org/apache/camel/component/fastjson/FastjsonDataFormat.java
+++ 
b/components/camel-fastjson/src/main/java/org/apache/camel/component/fastjson/FastjsonDataFormat.java
@@ -33,6 +33,7 @@ import org.apache.camel.spi.DataFormatContentTypeHeader;
 import org.apache.camel.spi.DataFormatName;
 import org.apache.camel.spi.Metadata;
 import org.apache.camel.spi.annotations.Dataformat;
+import org.apache.camel.support.JsonPayloadHelper;
 import org.apache.camel.support.service.ServiceSupport;
 
 /**
@@ -92,6 +93,16 @@ public class FastjsonDataFormat extends ServiceSupport
 
     @Override
     public void marshal(final Exchange exchange, final Object graph, final 
OutputStream stream) throws Exception {
+        // a body that already is the JSON text is written as it is 
(CAMEL-25329)
+        long written = JsonPayloadHelper.writeIfAlreadyJson(exchange, graph, 
stream);
+        if (written >= 0) {
+            if (contentTypeHeader) {
+                Message message = exchange.getMessage();
+                message.setHeader(Exchange.CONTENT_TYPE, "application/json");
+                message.setHeader(Exchange.CONTENT_LENGTH, written);
+            }
+            return;
+        }
         int len = JSON.writeJSONString(stream,
                 config.getCharset(),
                 graph,
diff --git 
a/components/camel-fastjson/src/test/java/org/apache/camel/component/fastjson/FastjsonMarshalJsonTextTest.java
 
b/components/camel-fastjson/src/test/java/org/apache/camel/component/fastjson/FastjsonMarshalJsonTextTest.java
new file mode 100644
index 000000000000..3c3cabe0e46b
--- /dev/null
+++ 
b/components/camel-fastjson/src/test/java/org/apache/camel/component/fastjson/FastjsonMarshalJsonTextTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.component.fastjson;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.file.GenericFile;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * A body that is already the JSON text is written as it is: a file, a stream, 
bytes, or a String holding a JSON object
+ * or array (CAMEL-25329). Any other String and every object is marshalled as 
before.
+ */
+public class FastjsonMarshalJsonTextTest extends CamelTestSupport {
+
+    private static final String JSON = "{\"sku\": \"CAMEL-MUG\", \"qty\": 2}";
+
+    @TempDir
+    static Path dir;
+
+    private String marshal(Object body) {
+        Object out = template.requestBody("direct:marshal", body);
+        return context.getTypeConverter().convertTo(String.class, out);
+    }
+
+    @Test
+    public void aStringWithAJsonObjectOrArrayIsWrittenAsIs() {
+        assertEquals(JSON, marshal(JSON));
+        assertEquals("[" + JSON + "]", marshal("[" + JSON + "]"));
+        assertEquals("  " + JSON + "\n", marshal("  " + JSON + "\n"));
+    }
+
+    @Test
+    public void anyOtherStringIsAJsonString() {
+        assertEquals("\"hello\"", marshal("hello"));
+        assertEquals("\"{not closed\"", marshal("{not closed"));
+    }
+
+    @Test
+    public void bytesAndStreamsAreWrittenAsIs() {
+        assertEquals(JSON, marshal(JSON.getBytes(StandardCharsets.UTF_8)));
+        assertEquals(JSON, marshal(new 
ByteArrayInputStream(JSON.getBytes(StandardCharsets.UTF_8))));
+    }
+
+    @Test
+    public void objectsAreMarshalledAsBefore() {
+        assertEquals("{\"sku\":\"CAMEL-MUG\"}", marshal(Map.of("sku", 
"CAMEL-MUG")));
+    }
+
+    @Test
+    public void aFileIsWrittenAsIs() throws Exception {
+        // the body a file consumer hands on: a GenericFile of the file
+        Path file = Files.writeString(dir.resolve("item.json"), JSON);
+        GenericFile<File> body = new GenericFile<>();
+        body.setFile(file.toFile());
+        body.setFileName("item.json");
+        body.setAbsoluteFilePath(file.toString());
+        body.setBody(file.toFile());
+
+        assertEquals(JSON, marshal(body));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                FastjsonDataFormat json = new FastjsonDataFormat();
+                from("direct:marshal").marshal(json);
+            }
+        };
+    }
+}
diff --git a/components/camel-gson/src/main/docs/gson-dataformat.adoc 
b/components/camel-gson/src/main/docs/gson-dataformat.adoc
index a64e89343a85..07dfe44bf0de 100644
--- a/components/camel-gson/src/main/docs/gson-dataformat.adoc
+++ b/components/camel-gson/src/main/docs/gson-dataformat.adoc
@@ -58,6 +58,14 @@ include::partial$dataformat-options.adoc[]
 // dataformat options: END
 
 
+== A body that is already JSON
+
+A body that already is the JSON text is written as it is, not serialized as a 
Java object: a file, an `InputStream`,
+a `byte[]`, and a `String` whose text is a JSON object or array (it starts 
with `{` and ends with `}`, or `[` and
+`]`). So `marshal: json` after reading a JSON file, or after building the JSON 
with a template or a Simple
+expression, sends that JSON unchanged. Any other `String` is written as a JSON 
string, and POJOs, maps and lists are
+marshalled as usual. All the JSON data formats of Camel do the same.
+
 == Dependencies
 
 To use Gson in your camel routes, you need to add the dependency
diff --git 
a/components/camel-gson/src/main/java/org/apache/camel/component/gson/GsonDataFormat.java
 
b/components/camel-gson/src/main/java/org/apache/camel/component/gson/GsonDataFormat.java
index bbeaf5ee58e9..2b68154a8acb 100644
--- 
a/components/camel-gson/src/main/java/org/apache/camel/component/gson/GsonDataFormat.java
+++ 
b/components/camel-gson/src/main/java/org/apache/camel/component/gson/GsonDataFormat.java
@@ -42,6 +42,7 @@ import org.apache.camel.spi.DataFormatName;
 import org.apache.camel.spi.Metadata;
 import org.apache.camel.spi.annotations.Dataformat;
 import org.apache.camel.support.ExchangeHelper;
+import org.apache.camel.support.JsonPayloadHelper;
 import org.apache.camel.support.service.ServiceSupport;
 import org.apache.camel.util.IOHelper;
 
@@ -141,6 +142,13 @@ public class GsonDataFormat extends ServiceSupport
 
     @Override
     public void marshal(final Exchange exchange, final Object graph, final 
OutputStream stream) throws Exception {
+        // a body that already is the JSON text is written as it is 
(CAMEL-25329)
+        if (JsonPayloadHelper.writeIfAlreadyJson(exchange, graph, stream) >= 
0) {
+            if (contentTypeHeader) {
+                exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, 
"application/json");
+            }
+            return;
+        }
         try (final OutputStreamWriter osw = new OutputStreamWriter(stream, 
ExchangeHelper.getCharsetName(exchange));
              final BufferedWriter writer = IOHelper.buffered(osw)) {
             gson.toJson(graph, writer);
diff --git 
a/components/camel-gson/src/test/java/org/apache/camel/component/gson/GsonMarshalJsonTextTest.java
 
b/components/camel-gson/src/test/java/org/apache/camel/component/gson/GsonMarshalJsonTextTest.java
new file mode 100644
index 000000000000..6b06323be92e
--- /dev/null
+++ 
b/components/camel-gson/src/test/java/org/apache/camel/component/gson/GsonMarshalJsonTextTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.component.gson;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.file.GenericFile;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * A body that is already the JSON text is written as it is: a file, a stream, 
bytes, or a String holding a JSON object
+ * or array (CAMEL-25329). Any other String and every object is marshalled as 
before.
+ */
+public class GsonMarshalJsonTextTest extends CamelTestSupport {
+
+    private static final String JSON = "{\"sku\": \"CAMEL-MUG\", \"qty\": 2}";
+
+    @TempDir
+    static Path dir;
+
+    private String marshal(Object body) {
+        Object out = template.requestBody("direct:marshal", body);
+        return context.getTypeConverter().convertTo(String.class, out);
+    }
+
+    @Test
+    public void aStringWithAJsonObjectOrArrayIsWrittenAsIs() {
+        assertEquals(JSON, marshal(JSON));
+        assertEquals("[" + JSON + "]", marshal("[" + JSON + "]"));
+        assertEquals("  " + JSON + "\n", marshal("  " + JSON + "\n"));
+    }
+
+    @Test
+    public void anyOtherStringIsAJsonString() {
+        assertEquals("\"hello\"", marshal("hello"));
+        assertEquals("\"{not closed\"", marshal("{not closed"));
+    }
+
+    @Test
+    public void bytesAndStreamsAreWrittenAsIs() {
+        assertEquals(JSON, marshal(JSON.getBytes(StandardCharsets.UTF_8)));
+        assertEquals(JSON, marshal(new 
ByteArrayInputStream(JSON.getBytes(StandardCharsets.UTF_8))));
+    }
+
+    @Test
+    public void objectsAreMarshalledAsBefore() {
+        assertEquals("{\"sku\":\"CAMEL-MUG\"}", marshal(Map.of("sku", 
"CAMEL-MUG")));
+    }
+
+    @Test
+    public void aFileIsWrittenAsIs() throws Exception {
+        // the body a file consumer hands on: a GenericFile of the file
+        Path file = Files.writeString(dir.resolve("item.json"), JSON);
+        GenericFile<File> body = new GenericFile<>();
+        body.setFile(file.toFile());
+        body.setFileName("item.json");
+        body.setAbsoluteFilePath(file.toString());
+        body.setBody(file.toFile());
+
+        assertEquals(JSON, marshal(body));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                GsonDataFormat json = new GsonDataFormat();
+                from("direct:marshal").marshal(json);
+            }
+        };
+    }
+}
diff --git a/components/camel-jackson/src/main/docs/jackson2-dataformat.adoc 
b/components/camel-jackson/src/main/docs/jackson2-dataformat.adoc
index d1d8490a985d..96e49f1ee222 100644
--- a/components/camel-jackson/src/main/docs/jackson2-dataformat.adoc
+++ b/components/camel-jackson/src/main/docs/jackson2-dataformat.adoc
@@ -66,6 +66,14 @@ compliant with Json specification. This can however 
frustrate users, because it
 names and texts with Unicode escapes. To avoid this, users commonly use 
4-bytes would need to turn on `combineUnicodeSurrogates=true`
 in the Camel dataformat.
 
+=== A body that is already JSON
+
+A body that already is the JSON text is written as it is, not serialized as a 
Java object: a file, an `InputStream`,
+a `byte[]`, and a `String` whose text is a JSON object or array (it starts 
with `{` and ends with `}`, or `[` and
+`]`). So `marshal: json` after reading a JSON file, or after building the JSON 
with a template or a Simple
+expression, sends that JSON unchanged. Any other `String` is written as a JSON 
string, and POJOs, maps and lists are
+marshalled as usual. All the JSON data formats of Camel do the same.
+
 === Using custom ObjectMapper
 
 You can configure `JacksonDataFormat` to use a custom `ObjectMapper` in case 
you need more control of the mapping configuration.
diff --git 
a/components/camel-jackson/src/main/java/org/apache/camel/component/jackson/JacksonDataFormat.java
 
b/components/camel-jackson/src/main/java/org/apache/camel/component/jackson/JacksonDataFormat.java
index 1ff4cd689f45..e01f3254ec18 100644
--- 
a/components/camel-jackson/src/main/java/org/apache/camel/component/jackson/JacksonDataFormat.java
+++ 
b/components/camel-jackson/src/main/java/org/apache/camel/component/jackson/JacksonDataFormat.java
@@ -16,12 +16,16 @@
  */
 package org.apache.camel.component.jackson;
 
+import java.io.OutputStream;
+
 import com.fasterxml.jackson.core.StreamReadConstraints;
 import com.fasterxml.jackson.databind.MapperFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.json.JsonMapper;
+import org.apache.camel.Exchange;
 import org.apache.camel.spi.Metadata;
 import org.apache.camel.spi.annotations.Dataformat;
+import org.apache.camel.support.JsonPayloadHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -110,4 +114,19 @@ public class JacksonDataFormat extends 
AbstractJacksonDataFormat {
         return "application/json";
     }
 
+    /**
+     * A body that is already the JSON text is written as it is (CAMEL-25329), 
see {@link JsonPayloadHelper}: a file, a
+     * stream, bytes, or a String holding a JSON object or array. Any other 
String, and every object, is marshalled as
+     * before.
+     */
+    @Override
+    public void marshal(Exchange exchange, Object graph, OutputStream stream) 
throws Exception {
+        if (JsonPayloadHelper.writeIfAlreadyJson(exchange, graph, stream) >= 
0) {
+            if (isContentTypeHeader()) {
+                exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, 
getDefaultContentType());
+            }
+            return;
+        }
+        super.marshal(exchange, graph, stream);
+    }
 }
diff --git 
a/components/camel-jackson/src/test/java/org/apache/camel/component/jackson/JacksonMarshalJsonTextTest.java
 
b/components/camel-jackson/src/test/java/org/apache/camel/component/jackson/JacksonMarshalJsonTextTest.java
new file mode 100644
index 000000000000..c32bbc62e89e
--- /dev/null
+++ 
b/components/camel-jackson/src/test/java/org/apache/camel/component/jackson/JacksonMarshalJsonTextTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.component.jackson;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.file.GenericFile;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * A body that is already the JSON text is written as it is: a file, a stream, 
bytes, or a String holding a JSON object
+ * or array (CAMEL-25329). Any other String and every object is marshalled as 
before.
+ */
+public class JacksonMarshalJsonTextTest extends CamelTestSupport {
+
+    private static final String JSON = "{\"sku\": \"CAMEL-MUG\", \"qty\": 2}";
+
+    @TempDir
+    static Path dir;
+
+    private String marshal(Object body) {
+        Object out = template.requestBody("direct:marshal", body);
+        return context.getTypeConverter().convertTo(String.class, out);
+    }
+
+    @Test
+    public void aStringWithAJsonObjectOrArrayIsWrittenAsIs() {
+        assertEquals(JSON, marshal(JSON));
+        assertEquals("[" + JSON + "]", marshal("[" + JSON + "]"));
+        assertEquals("  " + JSON + "\n", marshal("  " + JSON + "\n"));
+    }
+
+    @Test
+    public void anyOtherStringIsAJsonString() {
+        assertEquals("\"hello\"", marshal("hello"));
+        assertEquals("\"{not closed\"", marshal("{not closed"));
+    }
+
+    @Test
+    public void bytesAndStreamsAreWrittenAsIs() {
+        assertEquals(JSON, marshal(JSON.getBytes(StandardCharsets.UTF_8)));
+        assertEquals(JSON, marshal(new 
ByteArrayInputStream(JSON.getBytes(StandardCharsets.UTF_8))));
+    }
+
+    @Test
+    public void objectsAreMarshalledAsBefore() {
+        assertEquals("{\"sku\":\"CAMEL-MUG\"}", marshal(Map.of("sku", 
"CAMEL-MUG")));
+    }
+
+    @Test
+    public void aFileIsWrittenAsIs() throws Exception {
+        // the body a file consumer hands on: a GenericFile of the file
+        Path file = Files.writeString(dir.resolve("item.json"), JSON);
+        GenericFile<File> body = new GenericFile<>();
+        body.setFile(file.toFile());
+        body.setFileName("item.json");
+        body.setAbsoluteFilePath(file.toString());
+        body.setBody(file.toFile());
+
+        assertEquals(JSON, marshal(body));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                JacksonDataFormat json = new JacksonDataFormat();
+                from("direct:marshal").marshal(json);
+            }
+        };
+    }
+}
diff --git a/components/camel-jackson3/src/main/docs/jackson3-dataformat.adoc 
b/components/camel-jackson3/src/main/docs/jackson3-dataformat.adoc
index dc7f2530a71f..149c8ff60499 100644
--- a/components/camel-jackson3/src/main/docs/jackson3-dataformat.adoc
+++ b/components/camel-jackson3/src/main/docs/jackson3-dataformat.adoc
@@ -94,6 +94,14 @@ compliant with Json specification. This can however 
frustrate users, because it
 names and texts with Unicode escapes. To avoid this, users commonly use 
4-bytes would need to turn on `combineUnicodeSurrogates=true`
 in the Camel dataformat.
 
+=== A body that is already JSON
+
+A body that already is the JSON text is written as it is, not serialized as a 
Java object: a file, an `InputStream`,
+a `byte[]`, and a `String` whose text is a JSON object or array (it starts 
with `{` and ends with `}`, or `[` and
+`]`). So `marshal: json` after reading a JSON file, or after building the JSON 
with a template or a Simple
+expression, sends that JSON unchanged. Any other `String` is written as a JSON 
string, and POJOs, maps and lists are
+marshalled as usual. All the JSON data formats of Camel do the same.
+
 === Using custom ObjectMapper
 
 You can configure `JacksonDataFormat` to use a custom `ObjectMapper` in case 
you need more control of the mapping configuration.
diff --git 
a/components/camel-jackson3/src/main/java/org/apache/camel/component/jackson3/JacksonDataFormat.java
 
b/components/camel-jackson3/src/main/java/org/apache/camel/component/jackson3/JacksonDataFormat.java
index cba5a38abf6f..7fbc2e7d9618 100644
--- 
a/components/camel-jackson3/src/main/java/org/apache/camel/component/jackson3/JacksonDataFormat.java
+++ 
b/components/camel-jackson3/src/main/java/org/apache/camel/component/jackson3/JacksonDataFormat.java
@@ -16,8 +16,12 @@
  */
 package org.apache.camel.component.jackson3;
 
+import java.io.OutputStream;
+
+import org.apache.camel.Exchange;
 import org.apache.camel.spi.Metadata;
 import org.apache.camel.spi.annotations.Dataformat;
+import org.apache.camel.support.JsonPayloadHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import tools.jackson.core.StreamReadConstraints;
@@ -113,4 +117,19 @@ public class JacksonDataFormat extends 
AbstractJacksonDataFormat {
         return "application/json";
     }
 
+    /**
+     * A body that is already the JSON text is written as it is (CAMEL-25329), 
see {@link JsonPayloadHelper}: a file, a
+     * stream, bytes, or a String holding a JSON object or array. Any other 
String, and every object, is marshalled as
+     * before.
+     */
+    @Override
+    public void marshal(Exchange exchange, Object graph, OutputStream stream) 
throws Exception {
+        if (JsonPayloadHelper.writeIfAlreadyJson(exchange, graph, stream) >= 
0) {
+            if (isContentTypeHeader()) {
+                exchange.getMessage().setHeader(Exchange.CONTENT_TYPE, 
getDefaultContentType());
+            }
+            return;
+        }
+        super.marshal(exchange, graph, stream);
+    }
 }
diff --git 
a/components/camel-jackson3/src/test/java/org/apache/camel/component/jackson3/JacksonMarshalJsonTextTest.java
 
b/components/camel-jackson3/src/test/java/org/apache/camel/component/jackson3/JacksonMarshalJsonTextTest.java
new file mode 100644
index 000000000000..3feffe338d14
--- /dev/null
+++ 
b/components/camel-jackson3/src/test/java/org/apache/camel/component/jackson3/JacksonMarshalJsonTextTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.component.jackson3;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.file.GenericFile;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * A body that is already the JSON text is written as it is: a file, a stream, 
bytes, or a String holding a JSON object
+ * or array (CAMEL-25329). Any other String and every object is marshalled as 
before.
+ */
+public class JacksonMarshalJsonTextTest extends CamelTestSupport {
+
+    private static final String JSON = "{\"sku\": \"CAMEL-MUG\", \"qty\": 2}";
+
+    @TempDir
+    static Path dir;
+
+    private String marshal(Object body) {
+        Object out = template.requestBody("direct:marshal", body);
+        return context.getTypeConverter().convertTo(String.class, out);
+    }
+
+    @Test
+    public void aStringWithAJsonObjectOrArrayIsWrittenAsIs() {
+        assertEquals(JSON, marshal(JSON));
+        assertEquals("[" + JSON + "]", marshal("[" + JSON + "]"));
+        assertEquals("  " + JSON + "\n", marshal("  " + JSON + "\n"));
+    }
+
+    @Test
+    public void anyOtherStringIsAJsonString() {
+        assertEquals("\"hello\"", marshal("hello"));
+        assertEquals("\"{not closed\"", marshal("{not closed"));
+    }
+
+    @Test
+    public void bytesAndStreamsAreWrittenAsIs() {
+        assertEquals(JSON, marshal(JSON.getBytes(StandardCharsets.UTF_8)));
+        assertEquals(JSON, marshal(new 
ByteArrayInputStream(JSON.getBytes(StandardCharsets.UTF_8))));
+    }
+
+    @Test
+    public void objectsAreMarshalledAsBefore() {
+        assertEquals("{\"sku\":\"CAMEL-MUG\"}", marshal(Map.of("sku", 
"CAMEL-MUG")));
+    }
+
+    @Test
+    public void aFileIsWrittenAsIs() throws Exception {
+        // the body a file consumer hands on: a GenericFile of the file
+        Path file = Files.writeString(dir.resolve("item.json"), JSON);
+        GenericFile<File> body = new GenericFile<>();
+        body.setFile(file.toFile());
+        body.setFileName("item.json");
+        body.setAbsoluteFilePath(file.toString());
+        body.setBody(file.toFile());
+
+        assertEquals(JSON, marshal(body));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                JacksonDataFormat json = new JacksonDataFormat();
+                from("direct:marshal").marshal(json);
+            }
+        };
+    }
+}
diff --git a/components/camel-jsonb/src/main/docs/jsonb-dataformat.adoc 
b/components/camel-jsonb/src/main/docs/jsonb-dataformat.adoc
index b948621aa613..3d0a251f888f 100644
--- a/components/camel-jsonb/src/main/docs/jsonb-dataformat.adoc
+++ b/components/camel-jsonb/src/main/docs/jsonb-dataformat.adoc
@@ -57,6 +57,14 @@ include::partial$dataformat-options.adoc[]
 // dataformat options: END
 
 
+== A body that is already JSON
+
+A body that already is the JSON text is written as it is, not serialized as a 
Java object: a file, an `InputStream`,
+a `byte[]`, and a `String` whose text is a JSON object or array (it starts 
with `{` and ends with `}`, or `[` and
+`]`). So `marshal: json` after reading a JSON file, or after building the JSON 
with a template or a Simple
+expression, sends that JSON unchanged. Any other `String` is written as a JSON 
string, and POJOs, maps and lists are
+marshalled as usual. All the JSON data formats of Camel do the same.
+
 == Dependencies
 
 To use JSON-B in your Camel routes, you need to add the dependency
diff --git 
a/components/camel-jsonb/src/main/java/org/apache/camel/component/jsonb/JsonbDataFormat.java
 
b/components/camel-jsonb/src/main/java/org/apache/camel/component/jsonb/JsonbDataFormat.java
index 8386a19c4f29..22ce4dbf9a9a 100644
--- 
a/components/camel-jsonb/src/main/java/org/apache/camel/component/jsonb/JsonbDataFormat.java
+++ 
b/components/camel-jsonb/src/main/java/org/apache/camel/component/jsonb/JsonbDataFormat.java
@@ -31,10 +31,12 @@ import jakarta.json.bind.config.PropertyOrderStrategy;
 import org.apache.camel.CamelContext;
 import org.apache.camel.CamelContextAware;
 import org.apache.camel.Exchange;
+import org.apache.camel.RuntimeCamelException;
 import org.apache.camel.spi.DataFormat;
 import org.apache.camel.spi.DataFormatName;
 import org.apache.camel.spi.Metadata;
 import org.apache.camel.spi.annotations.Dataformat;
+import org.apache.camel.support.JsonPayloadHelper;
 import org.apache.camel.support.service.ServiceSupport;
 
 /**
@@ -181,6 +183,14 @@ public class JsonbDataFormat extends ServiceSupport 
implements DataFormat, DataF
 
     @Override
     public void marshal(Exchange exchange, Object graph, OutputStream stream) {
+        // a body that already is the JSON text is written as it is 
(CAMEL-25329)
+        try {
+            if (JsonPayloadHelper.writeIfAlreadyJson(exchange, graph, stream) 
>= 0) {
+                return;
+            }
+        } catch (Exception e) {
+            throw RuntimeCamelException.wrapRuntimeCamelException(e);
+        }
         objectMapper.toJson(graph, stream);
     }
 
diff --git 
a/components/camel-jsonb/src/test/java/org/apache/camel/component/jsonb/JsonbMarshalJsonTextTest.java
 
b/components/camel-jsonb/src/test/java/org/apache/camel/component/jsonb/JsonbMarshalJsonTextTest.java
new file mode 100644
index 000000000000..665d897824e3
--- /dev/null
+++ 
b/components/camel-jsonb/src/test/java/org/apache/camel/component/jsonb/JsonbMarshalJsonTextTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.component.jsonb;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.file.GenericFile;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * A body that is already the JSON text is written as it is: a file, a stream, 
bytes, or a String holding a JSON object
+ * or array (CAMEL-25329). Any other String and every object is marshalled as 
before.
+ */
+public class JsonbMarshalJsonTextTest extends CamelTestSupport {
+
+    private static final String JSON = "{\"sku\": \"CAMEL-MUG\", \"qty\": 2}";
+
+    @TempDir
+    static Path dir;
+
+    private String marshal(Object body) {
+        Object out = template.requestBody("direct:marshal", body);
+        return context.getTypeConverter().convertTo(String.class, out);
+    }
+
+    @Test
+    public void aStringWithAJsonObjectOrArrayIsWrittenAsIs() {
+        assertEquals(JSON, marshal(JSON));
+        assertEquals("[" + JSON + "]", marshal("[" + JSON + "]"));
+        assertEquals("  " + JSON + "\n", marshal("  " + JSON + "\n"));
+    }
+
+    @Test
+    public void anyOtherStringIsAJsonString() {
+        assertEquals("\"hello\"", marshal("hello"));
+        assertEquals("\"{not closed\"", marshal("{not closed"));
+    }
+
+    @Test
+    public void bytesAndStreamsAreWrittenAsIs() {
+        assertEquals(JSON, marshal(JSON.getBytes(StandardCharsets.UTF_8)));
+        assertEquals(JSON, marshal(new 
ByteArrayInputStream(JSON.getBytes(StandardCharsets.UTF_8))));
+    }
+
+    @Test
+    public void objectsAreMarshalledAsBefore() {
+        assertEquals("{\"sku\":\"CAMEL-MUG\"}", marshal(Map.of("sku", 
"CAMEL-MUG")));
+    }
+
+    @Test
+    public void aFileIsWrittenAsIs() throws Exception {
+        // the body a file consumer hands on: a GenericFile of the file
+        Path file = Files.writeString(dir.resolve("item.json"), JSON);
+        GenericFile<File> body = new GenericFile<>();
+        body.setFile(file.toFile());
+        body.setFileName("item.json");
+        body.setAbsoluteFilePath(file.toString());
+        body.setBody(file.toFile());
+
+        assertEquals(JSON, marshal(body));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                JsonbDataFormat json = new JsonbDataFormat();
+                from("direct:marshal").marshal(json);
+            }
+        };
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/support/JsonPayloadHelperTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/support/JsonPayloadHelperTest.java
new file mode 100644
index 000000000000..25a68e472c9e
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/support/JsonPayloadHelperTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.support;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class JsonPayloadHelperTest extends ContextTestSupport {
+
+    private static final String JSON = "{\"sku\": \"CAMEL-MUG\"}";
+
+    @Test
+    public void jsonTextIsAnObjectOrAnArray() {
+        assertTrue(JsonPayloadHelper.isJsonText(JSON));
+        assertTrue(JsonPayloadHelper.isJsonText("  [1, 2]\n"));
+        assertFalse(JsonPayloadHelper.isJsonText("hello"));
+        assertFalse(JsonPayloadHelper.isJsonText("{not closed"));
+        assertFalse(JsonPayloadHelper.isJsonText("\"a string\""));
+        assertFalse(JsonPayloadHelper.isJsonText(null));
+    }
+
+    @Test
+    public void aPayloadIsWrittenAsItIs() throws Exception {
+        Exchange exchange = createExchangeWithBody(null);
+        byte[] bytes = JSON.getBytes(StandardCharsets.UTF_8);
+        for (Object body : new Object[] { JSON, bytes, new 
ByteArrayInputStream(bytes) }) {
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            assertEquals(bytes.length, 
JsonPayloadHelper.writeIfAlreadyJson(exchange, body, out), "for " + body);
+            assertEquals(JSON, out.toString(StandardCharsets.UTF_8));
+        }
+    }
+
+    @Test
+    public void anythingElseIsLeftToTheDataFormat() throws Exception {
+        Exchange exchange = createExchangeWithBody(null);
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        assertEquals(-1, JsonPayloadHelper.writeIfAlreadyJson(exchange, 
"hello", out));
+        assertEquals(-1, JsonPayloadHelper.writeIfAlreadyJson(exchange, 
Map.of("sku", "CAMEL-MUG"), out));
+        assertEquals(0, out.size());
+    }
+}
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/JsonPayloadHelper.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/JsonPayloadHelper.java
new file mode 100644
index 000000000000..135e39fbc8be
--- /dev/null
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/JsonPayloadHelper.java
@@ -0,0 +1,84 @@
+/*
+ * 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.support;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.StreamCache;
+import org.apache.camel.WrappedFile;
+
+/**
+ * Helper for the JSON data formats: a body that already is the JSON text is 
written as it is when marshalling
+ * (CAMEL-25329). A file, a stream or bytes are the serialized form of a 
payload, never an object to serialize, and a
+ * String holding a JSON object or array is that same text; serializing them 
would fail, give Base64 or a number array,
+ * or encode the text as one JSON string. Any other String, and every object, 
is left to the data format.
+ */
+public final class JsonPayloadHelper {
+
+    private JsonPayloadHelper() {
+    }
+
+    /**
+     * Writes the body as it is when it already is the JSON text.
+     *
+     * @param  exchange  the exchange
+     * @param  body      the body to marshal
+     * @param  stream    where to write it
+     * @return           the number of bytes written, or -1 when the body is 
not already JSON (the data format marshals
+     *                   it as usual)
+     * @throws Exception when the body cannot be read
+     */
+    public static long writeIfAlreadyJson(Exchange exchange, Object body, 
OutputStream stream) throws Exception {
+        if (body instanceof byte[] bytes) {
+            stream.write(bytes);
+            return bytes.length;
+        } else if (body instanceof StreamCache cache) {
+            cache.writeTo(stream);
+            return cache.length();
+        } else if (body instanceof InputStream is) {
+            return is.transferTo(stream);
+        } else if (body instanceof WrappedFile<?> wf) {
+            // the file itself when there is one: no type conversion, which 
other converters could take part in
+            if (wf.getFile() instanceof File file) {
+                return Files.copy(file.toPath(), stream);
+            }
+            try (InputStream is
+                    = 
exchange.getContext().getTypeConverter().mandatoryConvertTo(InputStream.class, 
exchange, body)) {
+                return is.transferTo(stream);
+            }
+        } else if (body instanceof String text && isJsonText(text)) {
+            byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
+            stream.write(bytes);
+            return bytes.length;
+        }
+        return -1;
+    }
+
+    /** Whether the text is a JSON object or array, by its first and last 
non-blank character; it is not parsed. */
+    public static boolean isJsonText(String text) {
+        if (text == null) {
+            return false;
+        }
+        String t = text.strip();
+        return t.length() >= 2 && (t.startsWith("{") && t.endsWith("}") || 
t.startsWith("[") && t.endsWith("]"));
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index c0db19ae1876..3047cd978fbf 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -446,6 +446,21 @@ The Event developer console now exposes the full 
structured JSON payload in the
 each event entry, while keeping the existing flat `type`, `timestamp`, 
`exchangeId`, and
 `message` fields for backwards compatibility.
 
+=== JSON data formats - marshal a body that is already JSON text
+
+`marshal` with a JSON data format (`marshal: json`, with Jackson, Jackson 3, 
Gson, JSON-B or Fastjson) now writes a body
+that is already the JSON text as it is, instead of serializing it as a Java 
object:
+
+* a file, an `InputStream` (or stream cache) and a `byte[]` are written as 
they are; before, they were serialized as
+  objects (with Jackson the stream and the file failed with "No serializer 
found" and the bytes became a Base64
+  string)
+* a `String` whose text is a JSON object or array (it starts with `{` and ends 
with `}`, or `[` and `]`) is written as
+  it is; before, it was written as one JSON string, `"{\"sku\": ...}"`. Any 
other `String` is still written as a
+  JSON string (`"hello"`).
+
+POJOs, maps and lists are marshalled as before. A route that relied on a JSON 
`String` being encoded as a JSON
+string literal can wrap the value in an object, or set the body to the quoted 
text.
+
 === Intercept Send To Endpoint EIP
 
 The interceptors of `interceptSendToEndpoint` are now registered by each route 
while it is running, on an endpoint that

Reply via email to