[
https://issues.apache.org/jira/browse/CAMEL-24230?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18097944#comment-18097944
]
Omar Atie edited comment on CAMEL-24230 at 7/21/26 7:00 PM:
------------------------------------------------------------
CAMEL-24230(https://issues.apache.org/jira/browse/CAMEL-24230) is a real bug in
`camel-jsonpath`. The reporter's root-cause analysis is correct.
#
## The bug
With `writeAsString="true"`, a JSONPath that returns a *{*}single JSON
object{*}* (e.g. `$.args` → `\{age: 30, name: "Alice"}`) should produce a
*{*}JSON string{*}*:
```json
{"age":30,"name":"Alice"}
```
Instead you get a `java.util.Map` whose `toString()` looks like:
```
{age=30, name="Alice"}
```
#
## Root cause
In `JsonPathEngine.read()`, the `writeAsString` handling treats `Map`
differently from everything else:
```155:172:components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathEngine.java
} else if (answer instanceof Map) {
Map<Object, Object> map = (Map<Object, Object>) answer;
for (Map.Entry<Object, Object> entry : map.entrySet()) {
Object value = entry.getValue();
if (adapter != null) {
String json = adapter.writeAsString(value, exchange);
if (json != null)
{ map.put(entry.getKey(), json);
}
}
}
return map;
} else {
String json = adapter.writeAsString(answer, exchange);
if (json != null)
{ return json; }
}
```
Problems with this branch:
1. It *{*}returns the Map{*}*, not a `String`.
2. It only stringifies *{*}values{*}*, not the whole object.
3. Keys stay as Java map keys, so you get invalid/non-JSON output.
The `Iterable` branch (arrays) and the final `else` branch behave correctly —
they call `adapter.writeAsString(o, exchange)` and return JSON strings. Maps
are the outlier.
#
## The fix
Replace the `Map` branch with the same logic as the `else` branch — serialize
the *{*}entire{*}* object:
```java
} else if (answer instanceof Map) {
String json = adapter.writeAsString(answer, exchange);
if (json != null)
{ return json; }
}
```
Or simply *{*}remove the `Map` branch{*}* and let it fall through to the
existing `else`:
```java
if (answer instanceof Iterable) \{ // ... existing list handling ... } else
{
String json = adapter.writeAsString(answer, exchange);
if (json != null) \{ return json; }
}
```
`JacksonJsonAdapter.writeAsString()` already handles `Map` correctly via
`ObjectMapper.writeValueAsString(value)`.
#
## Regression to watch
There is one existing test that relies on the current (buggy) Map behavior:
`JsonPathSplitWriteAsStringMapTest` — splits on `$.content` where `content` is
a *{*}Map{*}* (`content-map.json`), and expects each split message to be a
`Map.Entry` with a stringified value.
After the fix, `writeAsString("$.content")` would return one JSON string for
the whole `content` object, not a Map of stringified values.
*{*}Update that test{*}* to use a path that returns an *{*}array{*}*, which is
the documented split pattern:
```java
.split().jsonpathWriteAsString("$.content.*") // returns array of inner objects
```
That aligns with `JsonPathSplitWriteAsStringTest`, which already works
correctly for array content.
#
## Tests to add
Add a test that reproduces the JIRA scenario:
```java
@Test
public void testWriteAsStringObjectExpression() throws Exception {
String json = """
{ "args": \\{ "age": 30, "name": "Alice" }
}
""";
MockEndpoint mock = getMockEndpoint("mock:result");
mock.expectedMessageCount(1);
mock.message(0).body().isInstanceOf(String.class);
mock.message(0).body().isEqualTo("\{\"age\":30,\"name\":\"Alice\"}");
template.sendBody("direct:start", json);
MockEndpoint.assertIsSatisfied(context);
}
```
Route:
```java
from("direct:start")
.setHeader("NewBody").jsonpath("$.args", true) // writeAsString=true
.to("mock:result");
```
Also test with XML DSL (`<jsonpath writeAsString="true">$.args</jsonpath>`) if
you want parity with the reporter's case.
#
## Workaround (until fixed)
If you need this today without patching Camel:
- Use *{*}`resultType` won't help{*}* — `TypeConverter` would still turn the
Map into `Map.toString()`.
- Use a *{*}JSONPath that returns an array{*}*, then take the first element:
```xml
<jsonpath writeAsString="true">$.args.[*]</jsonpath>
```
(depends on Jayway returning a list, which triggers the working `Iterable`
branch.)
- Or skip `writeAsString` and marshal with Jackson in a `.process()` /
`marshal().json()`.
—
This is a small, focused fix —
This answer is generated by cursor AI
was (Author: JIRAUSER313980):
[CAMEL-24230](https://issues.apache.org/jira/browse/CAMEL-24230) is a real bug
in `camel-jsonpath`. The reporter's root-cause analysis is correct.
## The bug
With `writeAsString="true"`, a JSONPath that returns a **single JSON object**
(e.g. `$.args` → `\{age: 30, name: "Alice"}`) should produce a **JSON string**:
```json
{"age":30,"name":"Alice"}
```
Instead you get a `java.util.Map` whose `toString()` looks like:
```
{age=30, name="Alice"}
```
## Root cause
In `JsonPathEngine.read()`, the `writeAsString` handling treats `Map`
differently from everything else:
```155:172:components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathEngine.java
} else if (answer instanceof Map) {
Map<Object, Object> map = (Map<Object, Object>) answer;
for (Map.Entry<Object, Object> entry : map.entrySet()) {
Object value = entry.getValue();
if (adapter != null) {
String json = adapter.writeAsString(value, exchange);
if (json != null) {
map.put(entry.getKey(), json);
}
}
}
return map;
} else {
String json = adapter.writeAsString(answer, exchange);
if (json != null) {
return json;
}
}
```
Problems with this branch:
1. It **returns the Map**, not a `String`.
2. It only stringifies **values**, not the whole object.
3. Keys stay as Java map keys, so you get invalid/non-JSON output.
The `Iterable` branch (arrays) and the final `else` branch behave correctly —
they call `adapter.writeAsString(o, exchange)` and return JSON strings. Maps
are the outlier.
## The fix
Replace the `Map` branch with the same logic as the `else` branch — serialize
the **entire** object:
```java
} else if (answer instanceof Map) {
String json = adapter.writeAsString(answer, exchange);
if (json != null) {
return json;
}
}
```
Or simply **remove the `Map` branch** and let it fall through to the existing
`else`:
```java
if (answer instanceof Iterable) {
// ... existing list handling ...
} else {
String json = adapter.writeAsString(answer, exchange);
if (json != null) {
return json;
}
}
```
`JacksonJsonAdapter.writeAsString()` already handles `Map` correctly via
`ObjectMapper.writeValueAsString(value)`.
## Regression to watch
There is one existing test that relies on the current (buggy) Map behavior:
`JsonPathSplitWriteAsStringMapTest` — splits on `$.content` where `content` is
a **Map** (`content-map.json`), and expects each split message to be a
`Map.Entry` with a stringified value.
After the fix, `writeAsString("$.content")` would return one JSON string for
the whole `content` object, not a Map of stringified values.
**Update that test** to use a path that returns an **array**, which is the
documented split pattern:
```java
.split().jsonpathWriteAsString("$.content.*") // returns array of inner objects
```
That aligns with `JsonPathSplitWriteAsStringTest`, which already works
correctly for array content.
## Tests to add
Add a test that reproduces the JIRA scenario:
```java
@Test
public void testWriteAsStringObjectExpression() throws Exception {
String json = """
{
"args": \{ "age": 30, "name": "Alice" }
}
""";
MockEndpoint mock = getMockEndpoint("mock:result");
mock.expectedMessageCount(1);
mock.message(0).body().isInstanceOf(String.class);
mock.message(0).body().isEqualTo("\{\"age\":30,\"name\":\"Alice\"}");
template.sendBody("direct:start", json);
MockEndpoint.assertIsSatisfied(context);
}
```
Route:
```java
from("direct:start")
.setHeader("NewBody").jsonpath("$.args", true) // writeAsString=true
.to("mock:result");
```
Also test with XML DSL (`<jsonpath writeAsString="true">$.args</jsonpath>`) if
you want parity with the reporter's case.
## Workaround (until fixed)
If you need this today without patching Camel:
- Use **`resultType` won't help** — `TypeConverter` would still turn the Map
into `Map.toString()`.
- Use a **JSONPath that returns an array**, then take the first element:
```xml
<jsonpath writeAsString="true">$.args.[*]</jsonpath>
```
(depends on Jayway returning a list, which triggers the working `Iterable`
branch.)
- Or skip `writeAsString` and marshal with Jackson in a `.process()` /
`marshal().json()`.
---
This is a small, focused fix — good candidate for a quick PR.
> jsonpath language with writeAsString=true returns Map instead of JSON String
> for object expressions
> ---------------------------------------------------------------------------------------------------
>
> Key: CAMEL-24230
> URL: https://issues.apache.org/jira/browse/CAMEL-24230
> Project: Camel
> Issue Type: Bug
> Components: camel-jsonpath
> Affects Versions: 4.21.0
> Reporter: Raymond
> Priority: Minor
>
> I have the following json:
> {code:java}
> {
> "args": {
> "age": 30,
> "name": "Alice"
> },
> "headers": {
> "Accept":
> "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
>
> "Accept-Encoding": "gzip, deflate, br, zstd",
> "Accept-Language": "nl-NL,nl;q=0.9,en-US;q=0.8,en;q=0.7",
> "Host": "httpbin.org",
> "Priority": "u=0, i",
> "Sec-Ch-Ua": "\"Google Chrome\";v=\"149\", \"Chromium\";v=\"149\",
> \"Not)A;Brand\";v=\"24\"",
> "Sec-Ch-Ua-Mobile": "?0",
> "Sec-Ch-Ua-Platform": "\"Windows\"",
> "Sec-Fetch-Dest": "document",
> "Sec-Fetch-Mode": "navigate",
> "Sec-Fetch-Site": "none",
> "Sec-Fetch-User": "?1",
> "Upgrade-Insecure-Requests": "1",
> "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)
> AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
> "X-Amzn-Trace-Id": "Root=1-6a2c1b7f-2c55fb0260804ec036ffb630"
> },
> "origin": "178.227.111.11",
> "url": "https://httpbin.org/get?name=Alice&age=30"
> }{code}
> And the following jsonpath:
>
> {code:java}
> $.args
> {code}
> The online jsonpath (https://jsonpath.com/) tester gives:
>
> {code:java}
> [ { "age": 30, "name": "Alice" }]{code}
> However when evaluating the same jsonpath in Apache Camel (with jsonpath on
> the classpath):
> {code:java}
> <setHeaders>
> <setHeader name="NewBody">
> <jsonpath writeAsString="true">$.args</jsonpath>
> </setHeader>
> </setHeaders> {code}
> I get:
> {{}}
> {code:java}
> {age=30, name="Alice"} {code}
> {{Thus the values are correct, but the keys not. }}When using the
> {{jsonpath}} expression language with {{writeAsString="true"}} on a query
> that evaluates to a single JSON Object (e.g., {{{}$.args{}}}),
> {{JsonPathEngine}} returns a {{java.util.Map<Object, Object>}} where the
> values are serialized, rather than serializing the entire Map into a valid
> JSON string.
> *Summary*
> {{}}
> *Expected Behavior:* {{<jsonpath writeAsString="true">$.args</jsonpath>}}
> should return a {{java.lang.String}} containing valid JSON (e.g.,
> {{{}{"age":30,"name":"Alice"}{}}}).
> {{}}
> *Actual Behavior:* It returns a {{java.util.Map}} whose {{.toString()}}
> evaluates to {{{}{age=30, name="Alice"}{}}}.
> {{}}
> *Possible Root Cause:* In {{{}org.apache.camel.jsonpath.JsonPathEngine{}}},
> the {{answer instanceof Map}} branch iterates over entries and stringifies
> {{{}entry.getValue(){}}}, but returns the {{Map}} instance itself instead of
> passing the entire {{answer}} object through
> {{{}adapter.writeAsString(answer, exchange){}}}.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)