[
https://issues.apache.org/jira/browse/CAMEL-25334?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Claus Ibsen updated CAMEL-25334:
--------------------------------
Description:
Low-code users, and AI models writing routes, think of the payload as JSON and
write data mappings in Groovy and simple accordingly. Camel carries the native
Java type of each component instead - a GenericFile, a byte[], an InputStream,
a Map - and that type changes during routing and from component to component.
The gap shows up as runtime errors:
* Groovy {{body.find \{ it.sku == headers.sku \}}} on JSON that was not
unmarshalled: {{No such property: sku for class: java.lang.Byte}}
* simple {{$\{body[amount]\}}} on the file consumer's body: {{Key: amount not
found in bean: GenericFile[...]}}
* a mapping that returns a Map, sent to an http producer: no type converter
from LinkedHashMap to InputStream
* a jsonpath result stored in a header as a LinkedList, which disappears when
the message crosses a JMS broker
Since then the hints of CAMEL-25330, CAMEL-25746 and CAMEL-25749 say what to do
in each of these cases, but nothing does it: on main {{$\{body.sku\}}},
{{$\{body[sku]\}}} and Groovy {{body.sku}} on JSON text all fail with
"unmarshal it first".
Raymond Meester described the same from the Fluxygen low-code platform on
Camel, where users write Groovy and simple without knowing Java:
[Text-orientated vs. Object-orientated in
integration|https://camel.zulipchat.com/#narrow/channel/257298-camel/topic/.E2.9C.94.20Text-orientated.20vs.2E.20Object-orientated.20in.20integration/with/568024059].
In the local-model benchmark, 19 of 156 attempts in one series died on such a
body type collision.
h3. Design: payloadMode
An opt-in mode where the route works on the payload as JSON, instead of the
various Java types that change during routing or from different components. It
is for low-code users and AI-built routes; classic routes keep the native body
types, and the mode is not meant for routes that want the lowest overhead.
*The switch*, like stream caching: {{payloadMode}} with the values {{default}}
(today's behaviour) and {{json}}, on the CamelContext ({{camel.main.payloadMode
= json}}) and per route, so a route can opt in or out. An enum leaves room for
other formats (XML) later.
*Inside the route the body is JSON data*: a {{JsonObject}} or {{JsonArray}} of
camel-util-json ({{JsonObject}} is a LinkedHashMap, so the field order is kept;
numbers are BigDecimal). So with no change to the languages or EIPs:
* Groovy: {{body.sku}}, {{body.items.each \{ ... \}}}, {{body.find \{ ... \}}}
work
* simple: {{$\{body.sku\}}} and {{$\{body[sku]\}}} work (the bean OGNL reads a
key of a Map)
* split of a JsonArray gives its elements, each a JsonObject; aggregating them
gives a List, which becomes a JsonArray
*Which bodies become JSON data*: the mode works where stream caching does, when
the consumer hands the message to the route and after every step, so one rule
covers the consumer's body, the reply of a producer and the result of any step:
||body||in json mode||
|Map, List (Groovy, process, sql, split, aggregate)|becomes JsonObject /
JsonArray|
|String, byte[], stream, file with a Content-Type of application/json or
*+json|parsed|
|the same without a Content-Type|parsed when the file name ends with .json
(MimeTypeHelper) or the text starts with \{ or [|
|another Content-Type (text/csv, application/xml, image/png ...)|as it is|
|JSON-looking text that does not parse ({{[INFO] started}})|as it is (logged at
DEBUG): the mode never fails a message|
|a stream that stream caching spooled to disk|as it is: the spool threshold is
the size limit, no new option|
|other Java objects (SDK objects, entities, JsonNode)|as they are (a JSON
writer SPI, for example from camel-jackson, can come later)|
A step that produces JSON text, including {{convertBodyTo: String}}, has it
parsed back into data; the text inside the route is {{$\{bodyAs(String)\}}}, or
{{body.toJson()}} in Groovy.
*At the edges it is JSON text*:
* new type converters from JsonObject / JsonArray to String, byte[] and
InputStream, so http, file, kafka and the other producers that read text, bytes
or a stream get the JSON text
* producers that take a Map as their API use the data: sql named parameters
({{:#sku}}), mongodb documents
* producers that would turn a Map into something that is not the JSON text are
fixed: JMS / SJMS send a JsonObject as a TextMessage, not a MapMessage (other
producers to be surveyed)
* header values that are a JsonObject, JsonArray, Map or List become JSON text
before a producer (not the Camel* headers), so they survive a JMS or Kafka hop
* direct / seda to another route pass the data as it is
*What users see is JSON text*: logs, tracing, the backlog tracer, the dev
consoles, Camel TUI and the MCP tools show the JSON text of the body (in
MessageHelper, or through a small display SPI).
h3. Plan
# camel-core: the payloadMode option (camel-api, camel-main, the model and the
YAML / XML DSL), the hook at the consumer and after every step with the rule
above, the converters to text, the header values before a producer, the
display, a user manual page, and tests that run the cases of the problem list
in both modes.
# components: JMS / SJMS send a JsonObject as a TextMessage, and a survey of
the producers that turn a Map into something else.
# tooling: the body flow of the validator, the catalog and Camel TUI / MCP know
payloadMode (in json mode the body of a file consumer is JSON data).
# Run the local-model benchmark with the mode off and on, and compare the body
type collisions and the pass rate.
Related: CAMEL-24844 (body type flow), CAMEL-25330 (the Groovy hint this
replaces with behaviour in json mode), CAMEL-25329 (marshal json writes JSON
text as it is).
_Claude Code on behalf of davsclaus_
was:
Low-code users, and AI models writing routes, think of the payload as JSON (or
XML, CSV, text) and write data mappings in Groovy and simple accordingly. Camel
carries the native Java type of each component instead - a GenericFile, a
byte[], an InputStream, a Map - and the gap shows up as runtime errors:
* Groovy {{body.find { it.sku == headers.sku } }} on JSON that was not
unmarshalled: {{No such property: sku for class: java.lang.Byte}}
* simple {{$\{body[amount]\}}} on the file consumer's body: {{Key: amount not
found in bean: GenericFile[...]}}
* a mapping that returns a Map, sent to an http producer: no type converter
from LinkedHashMap to InputStream
* a jsonpath result stored in a header as a LinkedList, which disappears when
the message crosses a JMS broker
Raymond Meester described the same from the Fluxygen low-code platform on
Camel, where users write Groovy and simple without knowing Java:
[Text-orientated vs. Object-orientated in
integration|https://camel.zulipchat.com/#narrow/channel/257298-camel/topic/.E2.9C.94.20Text-orientated.20vs.2E.20Object-orientated.20in.20integration/with/568024059].
In the local-model benchmark, 19 of 156 attempts in one series died on such a
body type collision.
h3. Proposal: a content-type aware mode
Not "everything is text", and not a second Camel: the payload keeps its content
type, the body stays the raw text or bytes between steps, and each language
reads it the way it expects:
||who||what it sees when the content type is JSON||
|jsonpath, jq|the raw JSON text, as today|
|Groovy|parsed data (Map / List), so {{body.sku}}, {{body.find \{ it.sku == ...
\}}}, {{body.items.each \{ ... \}}} work; the text stays available (for example
as {{bodyText}})|
|simple {{$\{body[sku]\}}}, {{$\{body.sku\}}}|the field, read from the parsed
JSON|
|producers|the text, unchanged|
|marshal json|the text, passed through (CAMEL-25329)|
The return side matters as much as the read side: a structured result (Map,
List, JsonNode) that a language stores with setBody or setHeader becomes JSON
text again, so the next producer, broker or mapping gets text and headers
survive a JMS hop.
Where the content type comes from: the Content-Type header, which HTTP
consumers and Jackson's marshal already set; for the file consumer, the file
name through MimeTypeHelper (probeContentType); otherwise the first character
of the text. Binary content stays binary, and a size threshold (as stream
caching has) keeps large payloads as streams.
Groovy needs no new dependency: camel-groovy already depends on groovy-json and
groovy-xml (JsonSlurper gives Map/List, XmlSlurper GPath for XML). Simple
already compares text numbers numerically (ObjectHelper.typeCoerceCompare), so
{{$\{header.qty\} > 5}} keeps working with text headers.
h3. Prototype and measurement
# Groovy, behind an option: a JSON body is bound as Map/List, the text as
bodyText, a Map/List result goes back to JSON text.
# simple: {{$\{body[x]\}}} and {{$\{body.x\}}} read fields from JSON text.
# Headers: a structured result from any language becomes JSON text when the
mode is on.
# Run the local-model benchmark ladder with the mode off and on and compare the
body type collisions and pass rate before deciding on the switch (one
camel.main property, or per language) and on defaults.
Classic routes keep the native body types; the mode is opt-in.
Related: CAMEL-24844 (body type flow), CAMEL-25330 (the Groovy hint this would
replace with behaviour), CAMEL-25329 (marshal passes JSON text through).
_Claude Code on behalf of davsclaus_
Summary: payloadMode=json for low-code: inside the route the body is
JSON data (JsonObject / JsonArray), at the edges JSON text (was: Content-type
aware payloads for low-code: the body stays text with its content type, Groovy
and simple read JSON fields from it, structured results go back to text)
> payloadMode=json for low-code: inside the route the body is JSON data
> (JsonObject / JsonArray), at the edges JSON text
> ----------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-25334
> URL: https://issues.apache.org/jira/browse/CAMEL-25334
> Project: Camel
> Issue Type: New Feature
> Components: camel-core, camel-groovy
> Reporter: Claus Ibsen
> Priority: Major
> Fix For: 4.24.0
>
>
> Low-code users, and AI models writing routes, think of the payload as JSON
> and write data mappings in Groovy and simple accordingly. Camel carries the
> native Java type of each component instead - a GenericFile, a byte[], an
> InputStream, a Map - and that type changes during routing and from component
> to component. The gap shows up as runtime errors:
> * Groovy {{body.find \{ it.sku == headers.sku \}}} on JSON that was not
> unmarshalled: {{No such property: sku for class: java.lang.Byte}}
> * simple {{$\{body[amount]\}}} on the file consumer's body: {{Key: amount not
> found in bean: GenericFile[...]}}
> * a mapping that returns a Map, sent to an http producer: no type converter
> from LinkedHashMap to InputStream
> * a jsonpath result stored in a header as a LinkedList, which disappears when
> the message crosses a JMS broker
> Since then the hints of CAMEL-25330, CAMEL-25746 and CAMEL-25749 say what to
> do in each of these cases, but nothing does it: on main {{$\{body.sku\}}},
> {{$\{body[sku]\}}} and Groovy {{body.sku}} on JSON text all fail with
> "unmarshal it first".
> Raymond Meester described the same from the Fluxygen low-code platform on
> Camel, where users write Groovy and simple without knowing Java:
> [Text-orientated vs. Object-orientated in
> integration|https://camel.zulipchat.com/#narrow/channel/257298-camel/topic/.E2.9C.94.20Text-orientated.20vs.2E.20Object-orientated.20in.20integration/with/568024059].
> In the local-model benchmark, 19 of 156 attempts in one series died on such
> a body type collision.
> h3. Design: payloadMode
> An opt-in mode where the route works on the payload as JSON, instead of the
> various Java types that change during routing or from different components.
> It is for low-code users and AI-built routes; classic routes keep the native
> body types, and the mode is not meant for routes that want the lowest
> overhead.
> *The switch*, like stream caching: {{payloadMode}} with the values
> {{default}} (today's behaviour) and {{json}}, on the CamelContext
> ({{camel.main.payloadMode = json}}) and per route, so a route can opt in or
> out. An enum leaves room for other formats (XML) later.
> *Inside the route the body is JSON data*: a {{JsonObject}} or {{JsonArray}}
> of camel-util-json ({{JsonObject}} is a LinkedHashMap, so the field order is
> kept; numbers are BigDecimal). So with no change to the languages or EIPs:
> * Groovy: {{body.sku}}, {{body.items.each \{ ... \}}}, {{body.find \{ ...
> \}}} work
> * simple: {{$\{body.sku\}}} and {{$\{body[sku]\}}} work (the bean OGNL reads
> a key of a Map)
> * split of a JsonArray gives its elements, each a JsonObject; aggregating
> them gives a List, which becomes a JsonArray
> *Which bodies become JSON data*: the mode works where stream caching does,
> when the consumer hands the message to the route and after every step, so one
> rule covers the consumer's body, the reply of a producer and the result of
> any step:
> ||body||in json mode||
> |Map, List (Groovy, process, sql, split, aggregate)|becomes JsonObject /
> JsonArray|
> |String, byte[], stream, file with a Content-Type of application/json or
> *+json|parsed|
> |the same without a Content-Type|parsed when the file name ends with .json
> (MimeTypeHelper) or the text starts with \{ or [|
> |another Content-Type (text/csv, application/xml, image/png ...)|as it is|
> |JSON-looking text that does not parse ({{[INFO] started}})|as it is (logged
> at DEBUG): the mode never fails a message|
> |a stream that stream caching spooled to disk|as it is: the spool threshold
> is the size limit, no new option|
> |other Java objects (SDK objects, entities, JsonNode)|as they are (a JSON
> writer SPI, for example from camel-jackson, can come later)|
> A step that produces JSON text, including {{convertBodyTo: String}}, has it
> parsed back into data; the text inside the route is {{$\{bodyAs(String)\}}},
> or {{body.toJson()}} in Groovy.
> *At the edges it is JSON text*:
> * new type converters from JsonObject / JsonArray to String, byte[] and
> InputStream, so http, file, kafka and the other producers that read text,
> bytes or a stream get the JSON text
> * producers that take a Map as their API use the data: sql named parameters
> ({{:#sku}}), mongodb documents
> * producers that would turn a Map into something that is not the JSON text
> are fixed: JMS / SJMS send a JsonObject as a TextMessage, not a MapMessage
> (other producers to be surveyed)
> * header values that are a JsonObject, JsonArray, Map or List become JSON
> text before a producer (not the Camel* headers), so they survive a JMS or
> Kafka hop
> * direct / seda to another route pass the data as it is
> *What users see is JSON text*: logs, tracing, the backlog tracer, the dev
> consoles, Camel TUI and the MCP tools show the JSON text of the body (in
> MessageHelper, or through a small display SPI).
> h3. Plan
> # camel-core: the payloadMode option (camel-api, camel-main, the model and
> the YAML / XML DSL), the hook at the consumer and after every step with the
> rule above, the converters to text, the header values before a producer, the
> display, a user manual page, and tests that run the cases of the problem list
> in both modes.
> # components: JMS / SJMS send a JsonObject as a TextMessage, and a survey of
> the producers that turn a Map into something else.
> # tooling: the body flow of the validator, the catalog and Camel TUI / MCP
> know payloadMode (in json mode the body of a file consumer is JSON data).
> # Run the local-model benchmark with the mode off and on, and compare the
> body type collisions and the pass rate.
> Related: CAMEL-24844 (body type flow), CAMEL-25330 (the Groovy hint this
> replaces with behaviour in json mode), CAMEL-25329 (marshal json writes JSON
> text as it is).
> _Claude Code on behalf of davsclaus_
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)