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 70bd1fc8defd CAMEL-25862: camel-jbang - the validator hint for a
rest-openapi call with parsed data offers the JSON text of the request first
(#28142)
70bd1fc8defd is described below
commit 70bd1fc8defd922b02ef3a41bd2a53600bf03f3f
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 20 12:35:26 2026 +0200
CAMEL-25862: camel-jbang - the validator hint for a rest-openapi call with
parsed data offers the JSON text of the request first (#28142)
CAMEL-25862: camel-jbang - the validator hint for a rest-openapi call with
parsed data offers the JSON text of the request first, with the fields it needs
The hint said to add marshal: json before the call. A model marshals the
data it has, but the request of an operation
is rarely that data: a line of an order has no orderId, which the request
of reserveStock needs, so every call was
answered 400. For a rest-openapi call with JSON data, the hint now offers
to set the body to the JSON text of the
request first, naming the fields the request needs (the required properties
of its schema, which OpenApiVerbs reads
from the specification, following a $ref), and marshal when the data has
those fields.
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../dsl/jbang/core/commands/ai/OpenApiVerbs.java | 106 ++++++++++++++++++---
.../dsl/jbang/core/commands/ai/ValidatorFacts.java | 2 +-
.../jbang/core/commands/ai/ValidatorFactsTest.java | 64 +++++++++++++
.../camel/dsl/yaml/validator/ProjectFacts.java | 12 ++-
.../camel/dsl/yaml/validator/TextBodyFlow.java | 49 ++++++++--
.../camel/dsl/yaml/validator/TextBodyFlowTest.java | 42 +++++++-
6 files changed, 250 insertions(+), 25 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/OpenApiVerbs.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/OpenApiVerbs.java
index 3c194461b574..1f359afb48d0 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/OpenApiVerbs.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/OpenApiVerbs.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -68,17 +69,7 @@ public final class OpenApiVerbs {
if (content == null || directory == null) {
return answer;
}
- Set<String> specs = new LinkedHashSet<>();
- for (Pattern pattern : List.of(SPECIFICATION, REST_OPENAPI_CALL,
SPECIFICATION_URI)) {
- Matcher m = pattern.matcher(content);
- while (m.find()) {
- specs.add(ProjectFacts.specName(m.group(1)));
- }
- }
- if (content.contains("rest-openapi")) {
- specs.add(ProjectFacts.specName(null));
- }
- for (String spec : specs) {
+ for (String spec : specs(content)) {
Map<?, ?> paths = spec.contains("://") || spec.contains("{{") ?
null : paths(directory, spec);
if (paths == null) {
continue;
@@ -99,14 +90,97 @@ public final class OpenApiVerbs {
return answer;
}
+ /**
+ * The fields the JSON request of each operation needs: the required
properties of its schema, following a $ref to
+ * the schemas of the components. By specification, found and keyed as
{@link #operations} does; an operation
+ * without a JSON request, or whose schema requires nothing, is left out.
A model that is told to marshal the data
+ * of a route sends it as it is, which an operation rarely takes.
+ */
+ public static Map<String, Map<String, List<String>>> requestFields(String
content, Path directory) {
+ Map<String, Map<String, List<String>>> answer = new LinkedHashMap<>();
+ if (content == null || directory == null) {
+ return answer;
+ }
+ for (String spec : specs(content)) {
+ Map<?, ?> root = spec.contains("://") || spec.contains("{{") ?
null : document(directory, spec);
+ if (root == null || !(root.get("paths") instanceof Map<?, ?>
paths)) {
+ continue;
+ }
+ Map<String, List<String>> fields = new LinkedHashMap<>();
+ for (Object path : paths.values()) {
+ if (path instanceof Map<?, ?> operations) {
+ for (Object operation : operations.values()) {
+ if (operation instanceof Map<?, ?> details &&
details.get("operationId") != null) {
+ List<String> required = required(root,
jsonSchema(details));
+ if (!required.isEmpty()) {
+
fields.put(String.valueOf(details.get("operationId")), required);
+ }
+ }
+ }
+ }
+ }
+ if (!fields.isEmpty()) {
+ answer.put(spec, fields);
+ }
+ }
+ return answer;
+ }
+
+ /** The specifications a file uses, as {@link #operations} finds them. */
+ private static Set<String> specs(String content) {
+ Set<String> specs = new LinkedHashSet<>();
+ for (Pattern pattern : List.of(SPECIFICATION, REST_OPENAPI_CALL,
SPECIFICATION_URI)) {
+ Matcher m = pattern.matcher(content);
+ while (m.find()) {
+ specs.add(ProjectFacts.specName(m.group(1)));
+ }
+ }
+ if (content.contains("rest-openapi")) {
+ specs.add(ProjectFacts.specName(null));
+ }
+ return specs;
+ }
+
+ /** The schema of the JSON request of an operation, or null. */
+ private static Object jsonSchema(Map<?, ?> operation) {
+ if (operation.get("requestBody") instanceof Map<?, ?> body &&
body.get("content") instanceof Map<?, ?> content
+ && content.get("application/json") instanceof Map<?, ?> json) {
+ return json.get("schema");
+ }
+ return null;
+ }
+
+ /** The required properties of a schema, following a $ref to the schemas
of the components. */
+ private static List<String> required(Map<?, ?> root, Object schema) {
+ if (schema instanceof Map<?, ?> s && s.get("$ref") instanceof String
ref
+ && ref.startsWith("#/components/schemas/")
+ && root.get("components") instanceof Map<?, ?> components
+ && components.get("schemas") instanceof Map<?, ?> schemas) {
+ schema =
schemas.get(ref.substring("#/components/schemas/".length()));
+ }
+ List<String> answer = new ArrayList<>();
+ if (schema instanceof Map<?, ?> s && s.get("required") instanceof
List<?> required) {
+ for (Object field : required) {
+ answer.add(String.valueOf(field));
+ }
+ }
+ return answer;
+ }
+
/** The paths of a specification beside the route, or in
src/main/resources; null when it cannot be read. */
private static Map<?, ?> paths(Path directory, String file) {
+ Map<?, ?> root = document(directory, file);
+ return root != null && root.get("paths") instanceof Map<?, ?> paths ?
paths : null;
+ }
+
+ /** A specification beside the route, or in src/main/resources; null when
it cannot be read. */
+ private static Map<?, ?> document(Path directory, String file) {
Path p = directory.resolve(file);
if (!Files.isRegularFile(p)) {
p = directory.resolve("src/main/resources").resolve(file);
}
try {
- return Files.isRegularFile(p) ? paths(Files.readString(p)) : null;
+ return Files.isRegularFile(p) ? document(Files.readString(p)) :
null;
} catch (Exception e) {
// an unreadable specification says nothing
return null;
@@ -146,6 +220,12 @@ public final class OpenApiVerbs {
}
static Map<?, ?> paths(String text) {
+ Map<?, ?> root = document(text);
+ return root != null && root.get("paths") instanceof Map<?, ?> paths ?
paths : null;
+ }
+
+ /** A specification as JSON or YAML; null when it cannot be read. */
+ private static Map<?, ?> document(String text) {
try {
Object document;
if (text.stripLeading().startsWith("{")) {
@@ -153,7 +233,7 @@ public final class OpenApiVerbs {
} else {
document = new Yaml(new SafeConstructor(new
LoaderOptions())).load(text);
}
- return document instanceof Map<?, ?> root && root.get("paths")
instanceof Map<?, ?> paths ? paths : null;
+ return document instanceof Map<?, ?> root ? root : null;
} catch (Exception e) {
// Ignore unreadable or unparseable JSON or YAML specifications.
return null;
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFacts.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFacts.java
index 601e7837a51a..a3ff7ef6b033 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFacts.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFacts.java
@@ -54,7 +54,7 @@ final class ValidatorFacts {
}
return new ProjectFacts(
OpenApiVerbs.bodylessEndpoints(content, directory),
OpenApiVerbs.operations(content, directory),
- restBindingMode(directory));
+ restBindingMode(directory),
OpenApiVerbs.requestFields(content, directory));
}
/**
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFactsTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFactsTest.java
index f20581ceb595..f720e453607b 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFactsTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ValidatorFactsTest.java
@@ -110,6 +110,70 @@ class ValidatorFactsTest {
assertThat(OpenApiVerbs.operations("- to: direct:x\n", dir)).isEmpty();
}
+ @Test
+ void theFieldsTheJsonRequestOfAnOperationNeeds(@TempDir Path dir) throws
Exception {
+ // a $ref to the schemas of the components, and an inline schema
+ Files.writeString(dir.resolve("stock-api.json"), """
+ {
+ "openapi": "3.0.2",
+ "paths": {
+ "/stock/{sku}": { "get": { "operationId": "getStock",
"responses": { "200": { "description": "ok" } } } },
+ "/stock/{sku}/reserve": { "post": { "operationId":
"reserveStock",
+ "requestBody": { "required": true, "content": {
"application/json": {
+ "schema": { "$ref": "#/components/schemas/Reservation"
} } } },
+ "responses": { "200": { "description": "ok" } } } }
+ },
+ "components": { "schemas": { "Reservation": { "type":
"object", "required": ["orderId", "qty"],
+ "properties": { "orderId": { "type": "string" }, "qty": {
"type": "integer" } } } } }
+ }
+ """);
+ Files.writeString(dir.resolve("orders-api.yaml"), """
+ openapi: 3.0.2
+ paths:
+ /orders:
+ post:
+ operationId: createOrder
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ required: [customer]
+ responses:
+ '201':
+ description: created
+ """);
+ String content = "- to: rest-openapi:stock-api.json#reserveStock\n-
to: rest-openapi:orders-api.yaml#createOrder\n";
+
+ assertThat(OpenApiVerbs.requestFields(content, dir)).isEqualTo(Map.of(
+ "stock-api.json", Map.of("reserveStock", List.of("orderId",
"qty")),
+ "orders-api.yaml", Map.of("createOrder",
List.of("customer"))));
+ // a specification without request schemas, such as STOCK_API, gives
none
+ Files.writeString(dir.resolve("stock-api.json"), STOCK_API);
+ assertThat(OpenApiVerbs.requestFields(content,
dir)).containsOnlyKeys("orders-api.yaml");
+ }
+
+ @Test
+ void theHintNamesTheFieldsOfTheRequest(@TempDir Path dir) throws Exception
{
+ Files.writeString(dir.resolve("stock-api.json"), """
+ {
+ "openapi": "3.0.2",
+ "paths": {
+ "/reservations": { "post": { "operationId": "reserveStock",
+ "requestBody": { "content": { "application/json": {
+ "schema": { "$ref": "#/components/schemas/Reservation"
} } } },
+ "responses": { "201": { "description": "ok" } } } }
+ },
+ "components": { "schemas": { "Reservation": { "required":
["orderId", "qty"] } } }
+ }
+ """);
+
+ List<String> msgs = SourceValidator.validate("orders.camel.yaml",
RESERVE, new DefaultCamelCatalog(), null, dir);
+ assertThat(msgs).anyMatch(m -> m.contains("set the body to the JSON
text of the request before this step"
+ + " (it needs orderId, qty),
such as with setBody and simple:"
+ + " '{\"orderId\": ...,
\"qty\": ...}'"));
+ }
+
@Test
void theRestBindingModeOfTheProperties(@TempDir Path dir) throws Exception
{
assertThat(ValidatorFacts.restBindingMode(dir)).isEqualTo("off");
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/ProjectFacts.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/ProjectFacts.java
index 865cbfe5c378..a45603d158bb 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/ProjectFacts.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/ProjectFacts.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.dsl.yaml.validator;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -30,9 +31,11 @@ import java.util.Set;
* operationId, in lower case
* @param restBindingMode the binding mode the project's properties set for
REST ({@code off} when they set none), or
* null when not known: then nothing is said that a
binding could change
+ * @param requestFields per specification, keyed as {@link #operations()}
is: the fields the JSON request of an
+ * operationId needs, for the operations whose
request schema requires some
*/
public record ProjectFacts(Set<String> bodylessEndpoints, Map<String,
Map<String, String>> operations,
- String restBindingMode) {
+ String restBindingMode, Map<String, Map<String, List<String>>>
requestFields) {
/** Nothing known about the project. */
public static final ProjectFacts NONE = new ProjectFacts(Set.of(),
Map.of(), null);
@@ -40,6 +43,13 @@ public record ProjectFacts(Set<String> bodylessEndpoints,
Map<String, Map<String
public ProjectFacts {
bodylessEndpoints = bodylessEndpoints != null ? bodylessEndpoints :
Set.of();
operations = operations != null ? operations : Map.of();
+ requestFields = requestFields != null ? requestFields : Map.of();
+ }
+
+ /** Without the fields of the requests. */
+ public ProjectFacts(Set<String> bodylessEndpoints, Map<String, Map<String,
String>> operations,
+ String restBindingMode) {
+ this(bodylessEndpoints, operations, restBindingMode, Map.of());
}
/**
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
index 241d056acf53..56a396ce1adf 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
@@ -181,14 +181,15 @@ final class TextBodyFlow {
/**
* What a walk knows besides the body: the route, the routes of the file,
where reports go (null for a route a step
* calls, which reports from its own walk), whether an http producer sends
the body with POST, and the verbs of the
- * OpenAPI operations a rest-openapi call can be judged by (empty when a
REST binding may write the body).
+ * OpenAPI operations a rest-openapi call can be judged by (empty when a
REST binding may write the body), with the
+ * fields their JSON requests need.
*/
private record Walk(String routeId, Map<String, JsonNode> byFrom,
List<Error> errors, Set<String> seen, boolean post,
- Map<String, Map<String, String>> operations) {
+ Map<String, Map<String, String>> operations, Map<String,
Map<String, List<String>>> requestFields) {
/** The walk of a route a step calls: for its outcome alone. */
Walk called() {
- return new Walk(null, byFrom, null, seen, false, operations);
+ return new Walk(null, byFrom, null, seen, false, operations,
requestFields);
}
}
@@ -199,6 +200,7 @@ final class TextBodyFlow {
// a rest-openapi call and a REST route can only be judged when no
REST binding can write the data
boolean unbound = restUnbound(target, facts);
Map<String, Map<String, String>> operations = unbound ?
facts.operations() : Map.of();
+ Map<String, Map<String, List<String>>> requestFields = unbound ?
facts.requestFields() : Map.of();
Set<String> restReplies = unbound ? restReplies(target, facts) :
Set.of();
Map<String, JsonNode> byFrom = new HashMap<>();
for (JsonNode entry : target) {
@@ -231,7 +233,9 @@ final class TextBodyFlow {
String uri = endpointOf(from);
String consumer = scheme(uri);
boolean post = NO_HTTP_HEADERS.contains(consumer) &&
!setsHttpHeaders(steps);
- Body end = walk(steps, consumerText(from), at, new Walk(id,
byFrom, errors, new HashSet<>(), post, operations));
+ Body end = walk(steps, consumerText(from), at, new Walk(
+ id, byFrom, errors, new HashSet<>(), post, operations,
+ requestFields));
// the body at the end of a platform-http route, or of the route
of a REST call, is the reply
boolean rest = uri != null && restReplies.contains(normalize(uri));
if (("platform-http".equals(consumer) || rest) && end instanceof
Parsed parsed && steps != null
@@ -565,12 +569,17 @@ final class TextBodyFlow {
}
/**
- * What a step that needs the body as text does with it, and what to
write; rest when a REST binding could write it.
+ * What a step that needs the body as text does with it, and what to
write; rest when a REST binding could write it;
+ * and for the call of an OpenAPI operation, the fields its request needs
(null for any other step).
*/
- private record Need(String what, String fix, boolean rest) {
+ private record Need(String what, String fix, boolean rest, List<String>
request) {
Need(String what, String fix) {
- this(what, fix, false);
+ this(what, fix, false, null);
+ }
+
+ Need(String what, String fix, boolean rest) {
+ this(what, fix, rest, null);
}
}
@@ -607,10 +616,12 @@ final class TextBodyFlow {
? w.operations().getOrDefault(operation.spec(),
Map.of()).get(operation.id())
: null;
if (verb != null &&
SENDS_A_BODY.contains(verb.toUpperCase(Locale.ROOT))) {
+ List<String> fields
+ = w.requestFields().getOrDefault(operation.spec(),
Map.of()).getOrDefault(operation.id(), List.of());
return new Need(
name + ": rest-openapi sends the body as the request
of the operation " + operation.id() + " ("
+ verb.toUpperCase(Locale.ROOT) + " in " +
operation.spec() + ")",
- "before this step", true);
+ "before this step", true, fields);
}
}
return null;
@@ -899,7 +910,27 @@ final class TextBodyFlow {
.format(new MessageFormat("{0}"))
.arguments((routeId != null ? "route " + routeId + ": " : "")
+ need.what()
+ ", but the body here is parsed data (a Map or a
List), not text - " + parsed.origin()
- + ": add marshal: " + parsed.format() + " " +
need.fix() + restBinding(need, parsed))
+ + ": " + fix(need, parsed))
.build();
}
+
+ /**
+ * What to write for parsed data where text is needed. The request of an
operation is rarely the data as it is, such
+ * as one line of an order that the request needs the id of the order for:
told to marshal, a model sends the data
+ * as it is. So the JSON text of the request comes first, with the fields
the request needs.
+ */
+ private static String fix(Need need, Parsed parsed) {
+ String marshal = "add marshal: " + parsed.format() + " " + need.fix();
+ if (need.request() == null || !"json".equals(parsed.format())) {
+ return marshal + restBinding(need, parsed);
+ }
+ List<String> fields = need.request();
+ String example = fields.isEmpty()
+ ? "{\"name\": \"${body[name]}\"}"
+ : "{" + String.join(", ", fields.stream().map(f -> "\"" + f +
"\": ...").toList()) + "}";
+ return "set the body to the JSON text of the request " + need.fix()
+ + (fields.isEmpty() ? "" : " (it needs " + String.join(", ",
fields) + ")")
+ + ", such as with setBody and simple: '" + example + "', or " +
marshal
+ + " when the data has the fields of the request" +
restBinding(need, parsed);
+ }
}
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
index 09940ee1f4a3..e961585b3b6d 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
@@ -597,7 +597,11 @@ public class TextBodyFlowTest {
assertThat(m).startsWith("Line 9: ");
assertThat(m).contains("route reserve: to: rest-openapi sends the
body as the request of the operation"
+ " reserveStock (POST in stock-api.json)");
- assertThat(m).contains("add marshal: json before this step, or let
the REST binding write it (bindingMode: json)");
+ // the JSON text of the request first: the data is rarely the
request as it is
+ assertThat(m).contains("unmarshal: json turns the text into data:
set the body to the JSON text of the request"
+ + " before this step, such as with setBody
and simple: '{\"name\": \"${body[name]}\"}',"
+ + " or add marshal: json before this step
when the data has the fields of the request,"
+ + " or let the REST binding write it
(bindingMode: json)");
});
// the same operation named by options, or with the specification as a
file: resource
String options = """
@@ -654,6 +658,42 @@ public class TextBodyFlowTest {
assertThat(lines(yaml.replace("expression: \"$.sku\"", "expression:
\"$\""), STOCK_API)).isEmpty();
}
+ @Test
+ public void testRestOpenApiCallNamesTheFieldsOfTheRequest() {
+ // r19 of the benchmark: told to marshal, the model sent each line as
JSON, without the orderId of the request
+ String yaml = """
+ - route:
+ id: reserve-order-lines
+ from:
+ uri: file:orders
+ steps:
+ - unmarshal:
+ json: {}
+ - split:
+ expression:
+ simple: "${body[lines]}"
+ steps:
+ - setHeader:
+ name: sku
+ expression:
+ simple: "${body[sku]}"
+ - to:
+ uri: rest-openapi
+ parameters:
+ specificationUri: stock-api.json
+ operationId: reserveStock
+ """;
+ ProjectFacts facts = new ProjectFacts(
+ Set.of(), STOCK_API.operations(), "off",
+ Map.of("stock-api.json", Map.of("reserveStock",
List.of("orderId", "qty"))));
+ assertThat(lines(yaml, facts)).singleElement().satisfies(m -> {
+ assertThat(m).contains("rest-openapi sends the body as the request
of the operation reserveStock");
+ assertThat(m).contains("set the body to the JSON text of the
request before this step (it needs orderId, qty),"
+ + " such as with setBody and simple:
'{\"orderId\": ..., \"qty\": ...}', or add marshal:"
+ + " json before this step when the data has
the fields of the request");
+ });
+ }
+
@Test
public void testRestOpenApiCallsThatAreFine() {
String post = """