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 6ca1a201c3f7 CAMEL-25347: Validate YAML scalar alternatives without
losing required fields (#27383)
6ca1a201c3f7 is described below
commit 6ca1a201c3f72bad523de821be78097d1a2431fb
Author: Luigi De Masi <[email protected]>
AuthorDate: Mon Oct 5 19:03:44 2026 +0200
CAMEL-25347: Validate YAML scalar alternatives without losing required
fields (#27383)
Co-authored-by: Codex <[email protected]>
---
.../org/apache/camel/catalog/models/param.json | 2 +-
.../apache/camel/catalog/schemas/camel-spring.xsd | 2 +-
.../apache/camel/catalog/schemas/camel-xml-io.xsd | 2 +-
.../camel/catalog/schemas/camelYamlDsl-model.json | 1 -
.../org/apache/camel/model/rest/param.json | 2 +-
.../apache/camel/model/rest/ParamDefinition.java | 2 +-
.../dsl/yaml/deserializers/ModelDeserializers.java | 2 +-
.../camel/dsl/yaml/validator/YamlValidator.java | 77 +++++-----------------
.../dsl/yaml/validator/SemanticSchemaTest.java | 77 ++++++++++++++++++++++
.../validator/YamlValidatorScalarLeniencyTest.java | 34 ++++++++++
.../resources/schema/camelYamlDsl-canonical.json | 2 +-
.../resources/schema/camelYamlDsl-model.json | 1 -
.../generated/resources/schema/camelYamlDsl.json | 2 +-
.../camel/dsl/yaml/YamlCompletionTreeTest.java | 10 +++
14 files changed, 145 insertions(+), 71 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/param.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/param.json
index da8a2d161c35..5a39a2bfac0b 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/param.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/param.json
@@ -14,7 +14,7 @@
"properties": {
"description": { "index": 0, "kind": "attribute", "displayName":
"Description", "group": "common", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "description": "Description of the parameter." },
"name": { "index": 1, "kind": "attribute", "displayName": "Name", "group":
"common", "required": true, "type": "string", "javaType": "java.lang.String",
"deprecated": false, "autowired": false, "secret": false, "description": "The
parameter name." },
- "type": { "index": 2, "kind": "attribute", "displayName": "Type", "group":
"common", "required": true, "type": "enum", "javaType":
"org.apache.camel.model.rest.RestParamType", "enum": [ "body", "formData",
"header", "path", "query" ], "deprecated": false, "autowired": false, "secret":
false, "defaultValue": "path", "description": "Sets the parameter type such as
body, form, header, path, or query." },
+ "type": { "index": 2, "kind": "attribute", "displayName": "Type", "group":
"common", "required": false, "type": "enum", "javaType":
"org.apache.camel.model.rest.RestParamType", "enum": [ "body", "formData",
"header", "path", "query" ], "deprecated": false, "autowired": false, "secret":
false, "defaultValue": "path", "description": "Sets the parameter type such as
body, form, header, path, or query." },
"defaultValue": { "index": 3, "kind": "attribute", "displayName": "Default
Value", "group": "common", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"description": "The default value of the parameter." },
"required": { "index": 4, "kind": "attribute", "displayName": "Required",
"group": "common", "required": false, "type": "boolean", "javaType":
"java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": true, "description": "Sets the parameter required flag." },
"collectionFormat": { "index": 5, "kind": "attribute", "displayName":
"Collection Format", "group": "common", "required": false, "type": "enum",
"javaType": "org.apache.camel.model.rest.CollectionFormat", "enum": [ "csv",
"multi", "pipes", "ssv", "tsv" ], "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "csv", "description": "Sets the parameter
collection format." },
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd
index 88bef026685b..b305ba561bbb 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd
@@ -16086,7 +16086,7 @@ The parameter name.
</xs:documentation>
</xs:annotation>
</xs:attribute>
- <xs:attribute name="type" type="tns:restParamType" use="required">
+ <xs:attribute name="type" type="tns:restParamType">
<xs:annotation>
<xs:documentation xml:lang="en">
<![CDATA[
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd
index 56f574fc39cc..6ade90f22464 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd
@@ -15229,7 +15229,7 @@ The parameter name.
</xs:documentation>
</xs:annotation>
</xs:attribute>
- <xs:attribute name="type" type="tns:restParamType" use="required">
+ <xs:attribute name="type" type="tns:restParamType">
<xs:annotation>
<xs:documentation xml:lang="en">
<![CDATA[
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
index 23600b53d7dd..79ce18977038 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
@@ -17887,7 +17887,6 @@
"description" : "Sets the parameter type such as body, form, header,
path, or query.",
"title" : "Type",
"default" : "path",
- "required" : true,
"displayName" : "Type",
"kind" : "attribute",
"index" : 2,
diff --git
a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/rest/param.json
b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/rest/param.json
index da8a2d161c35..5a39a2bfac0b 100644
---
a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/rest/param.json
+++
b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/rest/param.json
@@ -14,7 +14,7 @@
"properties": {
"description": { "index": 0, "kind": "attribute", "displayName":
"Description", "group": "common", "required": false, "type": "string",
"javaType": "java.lang.String", "deprecated": false, "autowired": false,
"secret": false, "description": "Description of the parameter." },
"name": { "index": 1, "kind": "attribute", "displayName": "Name", "group":
"common", "required": true, "type": "string", "javaType": "java.lang.String",
"deprecated": false, "autowired": false, "secret": false, "description": "The
parameter name." },
- "type": { "index": 2, "kind": "attribute", "displayName": "Type", "group":
"common", "required": true, "type": "enum", "javaType":
"org.apache.camel.model.rest.RestParamType", "enum": [ "body", "formData",
"header", "path", "query" ], "deprecated": false, "autowired": false, "secret":
false, "defaultValue": "path", "description": "Sets the parameter type such as
body, form, header, path, or query." },
+ "type": { "index": 2, "kind": "attribute", "displayName": "Type", "group":
"common", "required": false, "type": "enum", "javaType":
"org.apache.camel.model.rest.RestParamType", "enum": [ "body", "formData",
"header", "path", "query" ], "deprecated": false, "autowired": false, "secret":
false, "defaultValue": "path", "description": "Sets the parameter type such as
body, form, header, path, or query." },
"defaultValue": { "index": 3, "kind": "attribute", "displayName": "Default
Value", "group": "common", "required": false, "type": "string", "javaType":
"java.lang.String", "deprecated": false, "autowired": false, "secret": false,
"description": "The default value of the parameter." },
"required": { "index": 4, "kind": "attribute", "displayName": "Required",
"group": "common", "required": false, "type": "boolean", "javaType":
"java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false,
"defaultValue": true, "description": "Sets the parameter required flag." },
"collectionFormat": { "index": 5, "kind": "attribute", "displayName":
"Collection Format", "group": "common", "required": false, "type": "enum",
"javaType": "org.apache.camel.model.rest.CollectionFormat", "enum": [ "csv",
"multi", "pipes", "ssv", "tsv" ], "deprecated": false, "autowired": false,
"secret": false, "defaultValue": "csv", "description": "Sets the parameter
collection format." },
diff --git
a/core/camel-core-model/src/main/java/org/apache/camel/model/rest/ParamDefinition.java
b/core/camel-core-model/src/main/java/org/apache/camel/model/rest/ParamDefinition.java
index 54c8f04eb85b..35f88d62deaf 100644
---
a/core/camel-core-model/src/main/java/org/apache/camel/model/rest/ParamDefinition.java
+++
b/core/camel-core-model/src/main/java/org/apache/camel/model/rest/ParamDefinition.java
@@ -48,7 +48,7 @@ public class ParamDefinition {
@XmlAttribute(required = true)
@Metadata(description = "The parameter name.", required = true)
private String name;
- @XmlAttribute(required = true)
+ @XmlAttribute
@Metadata(description = "Sets the parameter type such as body, form,
header, path, or query.",
defaultValue = "path")
private RestParamType type;
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java
index 68b759f4741d..863c55758a50 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java
@@ -11583,7 +11583,7 @@ public final class ModelDeserializers extends
YamlDeserializerSupport {
@YamlProperty(name = "examples", type =
"array:org.apache.camel.model.rest.RestPropertyDefinition", description =
"Examples of the parameter.", displayName = "Examples"),
@YamlProperty(name = "name", type = "string", required =
true, description = "The parameter name.", displayName = "Name"),
@YamlProperty(name = "required", type = "boolean",
defaultValue = "true", description = "Sets the parameter required flag.",
displayName = "Required"),
- @YamlProperty(name = "type", type =
"enum:body,formData,header,path,query", required = true, defaultValue = "path",
description = "Sets the parameter type such as body, form, header, path, or
query.", displayName = "Type")
+ @YamlProperty(name = "type", type =
"enum:body,formData,header,path,query", defaultValue = "path", description =
"Sets the parameter type such as body, form, header, path, or query.",
displayName = "Type")
}
)
public static class ParamDefinitionDeserializer extends
YamlDeserializerBase<ParamDefinition> {
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
index f63f7e5b78cf..b8d4be27d31d 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
@@ -665,19 +665,15 @@ public class YamlValidator {
private List<Error> validate(JsonNode target, Set<String>
bodylessEndpoints) {
Schema against = isKamelet(target) ? kameletSchema : schema;
- // before the noise filter, which keeps one branch per construct: the
alternatives of a construct are the
- // keys its branches require, and they are only all present while
nothing has been dropped yet
- List<Error> raw = new ArrayList<>(against.validate(target));
- removeSatisfiedAlternatives(raw);
- var errors = filterOneOfNoise(raw);
+ // A branch only matches after scalar conversion if it has no other
errors, including missing required fields.
+ var errors = filterOneOfNoise(new
ArrayList<>(against.validate(target)));
// the checks below walk the document looking for Camel nodes, and
spec.definition of a Kamelet is a JSON
// schema of its properties, not Camel: a property named delay is not
the Delay EIP, which is what
// aws-s3-source and eight others were told. Removing the branch
leaves every path under spec.template as it
// was, so the locations and the lines stay right.
JsonNode camelOnly = isKamelet(target) ?
withoutKameletMetadata(target) : target;
- // the locations the runtime accepts after all, before they are
dropped: a branch of an anyOf that failed only
- // because of one of them leaves its companions behind, and those say
the file needs a property it does not
- // (fhir-sink unmarshals with fhirXml, whose fhirVersion is a
placeholder, and was told fhirJson was required)
+ // Keep accepted scalar locations to remove their enclosing
composition errors. Required fields remain
+ // mandatory even when another field in the same object accepts
placeholders or scalar conversion.
List<String> accepted = new ArrayList<>();
for (Error e : errors) {
if (isRuntimeAcceptedScalar(e)) {
@@ -688,7 +684,7 @@ public class YamlValidator {
if (!accepted.isEmpty()) {
errors.removeIf(e -> {
String keyword = e.getKeyword();
- if (!"required".equals(keyword) && !"oneOf".equals(keyword) &&
!"anyOf".equals(keyword)) {
+ if (!"oneOf".equals(keyword) && !"anyOf".equals(keyword)) {
return false;
}
String at = String.valueOf(e.getInstanceLocation());
@@ -1092,6 +1088,7 @@ public class YamlValidator {
}
String prefix = meta.getEvaluationPath().toString();
+ String instance = meta.getInstanceLocation().toString();
Map<String, List<Error>> branches = new LinkedHashMap<>();
for (Error e : errors) {
@@ -1099,7 +1096,9 @@ public class YamlValidator {
continue;
}
String path = e.getEvaluationPath().toString();
- if (path.startsWith(prefix + "/")) {
+ String location = e.getInstanceLocation().toString();
+ if (path.startsWith(prefix + "/")
+ && (location.equals(instance) ||
location.startsWith(instance + "/"))) {
String rest = path.substring(prefix.length() + 1);
String branchIndex = rest.contains("/") ?
rest.substring(0, rest.indexOf('/')) : rest;
branches.computeIfAbsent(branchIndex, k -> new
ArrayList<>()).add(e);
@@ -1110,17 +1109,19 @@ public class YamlValidator {
continue;
}
- // find the best-matching branch using a three-tier priority:
- // 1. property-level errors (additionalProperties, enum,
pattern, etc.) — "right branch, wrong value/property"
- // 2. type errors only — "wrong branch entirely" (less
informative)
- // 3. structural errors only (required, oneOf, not) —
wrong-branch noise
+ // find the best-matching branch using a four-tier priority:
+ // 1. only runtime-accepted scalar errors — a matching branch
once placeholders/conversions are allowed
+ // 2. property-level errors (additionalProperties, enum,
pattern, etc.) — "right branch, wrong value/property"
+ // 3. type errors only — "wrong branch entirely" (less
informative)
+ // 4. structural errors only (required, oneOf, not) —
wrong-branch noise
// within the same tier, prefer the deepest instance location
String bestBranch = null;
int bestDepth = -1;
int bestTier = 0;
for (Map.Entry<String, List<Error>> entry : branches.entrySet()) {
- int tier = branchTier(entry.getValue());
+ int tier =
entry.getValue().stream().allMatch(YamlValidator::isRuntimeAcceptedScalar)
+ ? 3 : branchTier(entry.getValue());
int maxDepth = entry.getValue().stream()
.mapToInt(e ->
e.getInstanceLocation().toString().length())
.max().orElse(0);
@@ -1276,52 +1277,6 @@ public class YamlValidator {
return groups;
}
- /**
- * Drops "required property X not found" where one of the alternatives is
in fact there.
- * <p/>
- * The branches of a {@code oneOf} each require their own key, and when
all of them are reported the file is told it
- * needs a key it does not: {@code unmarshal: {fhirXml: ...}} in one when
branch of a choice was told "required
- * property 'fhirJson' not found", because the winner of the other when
branch was chosen for it as well. Collected
- * per construct and per place, they name the keys the file did not write;
if the object at that place has anything
- * in it, a branch was chosen and the rest are noise. When it is empty,
the file really does have to pick one and
- * the errors stay (CAMEL-25238).
- */
- private static void removeSatisfiedAlternatives(List<Error> errors) {
- Map<String, List<Error>> groups = new LinkedHashMap<>();
- for (Error e : errors) {
- if (!"required".equals(e.getKeyword())) {
- continue;
- }
- String construct =
constructOf(String.valueOf(e.getEvaluationPath()));
- if (construct != null) {
- groups.computeIfAbsent(construct + "@" +
e.getInstanceLocation(), k -> new ArrayList<>()).add(e);
- }
- }
- for (List<Error> group : groups.values()) {
- JsonNode instance = group.get(0).getInstanceNode();
- if (instance == null || !instance.isObject()) {
- continue;
- }
- // the branches of a pick-one construct each require their own
key, so one error per key the file did not
- // write. The key it did write is the one missing from them: that
branch got past its required check and
- // failed, if at all, further in. So a non-empty object means a
branch was chosen and the rest are noise.
- // An object with nothing in it, or with a key no branch knows,
still gets the construct's own error and
- // the additionalProperties error, which is what actually tells
the author what to do.
- if (!instance.isEmpty() && group.size() > 1) {
- errors.removeAll(group);
- }
- }
- }
-
- /**
- * The schema construct an error came from when it came from inside a
branch of a {@code oneOf} or an {@code anyOf}:
- * its evaluation path up to and including that keyword, or null when it
is not inside one.
- */
- private static String constructOf(String evaluationPath) {
- int at = Math.max(evaluationPath.lastIndexOf("/oneOf/"),
evaluationPath.lastIndexOf("/anyOf/"));
- return at < 0 ? null : evaluationPath.substring(0, at + 7);
- }
-
/**
* Whether the schema rejected a scalar that the Camel runtime accepts, in
which case the error is dropped.
* <p>
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java
index 21ee9707c06e..cc28b5f68715 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java
@@ -22,6 +22,83 @@ import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThat;
class SemanticSchemaTest {
+ @ParameterizedTest
+ @ValueSource(booleans = { false, true })
+ void numericDecisionPoliciesAcceptRuntimeScalarForms(boolean canonical)
throws Exception {
+ var validator = new YamlValidator(canonical);
+ for (String value : new String[] { "0.1", "\"0.1\"", "\"{{limit}}\"",
"\"{{limit:0.1}}\"" }) {
+ assertThat(validator.validate(booleanQuestion(value)))
+ .as("decision policy %s in canonical mode %s", value,
canonical).isEmpty();
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { false, true })
+ void scalarConversionsInDifferentQuestionsSelectTheirOwnSchema(boolean
canonical) throws Exception {
+ var validator = new YamlValidator(canonical);
+ String questions = booleanQuestion("\"{{limit:0.1}}\"") + """
+ topic:
+ type: choice
+ instructions: Select the topic
+ criteria: {billing: 123, support: 456}
+ urgency:
+ type: score
+ instructions: Assess urgency
+ criteria: [1, 2, 3]
+ """;
+ assertThat(validator.validate(questions)).isEmpty();
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { false, true })
+ void placeholdersDoNotHideInvalidQuestionFields(boolean canonical) throws
Exception {
+ var validator = new YamlValidator(canonical);
+ String question = booleanQuestion("\"{{limit:0.1}}\"");
+ for (String invalid : new String[] {
+ question.replace("threshold:", "thresholdd:"),
+ question.replace("type: boolean", "type: choice\n
criteria: {billing: Payments}"),
+ question.replace("type: boolean", "type: score\n
criteria: [Low, High]"),
+ question.replace("threshold: \"{{limit:0.1}}\"", "threshold:
not-a-number"),
+ question.replace("threshold: \"{{limit:0.1}}\"", "threshold:
{value: 0.1}"),
+ question.replace("type: boolean", "type: unknown") }) {
+ assertThat(validator.validate(invalid)).as("invalid question: %s",
invalid).isNotEmpty();
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = { false, true })
+ void scalarConversionsDoNotHideMissingRequiredFields(boolean canonical)
throws Exception {
+ var validator = new YamlValidator(canonical);
+ for (String fields : new String[] {
+ "instructions: 123",
+ "instructions: Assess this\n threshold: \"{{limit}}\"",
+ "type: choice\n instructions: 123",
+ "type: score\n instructions: 123" }) {
+ String invalid = """
+ - semantic:
+ question:
+ invalid:
+ %s
+ """.formatted(fields);
+ String valid = booleanQuestion("\"{{limit:0.1}}\"");
+ for (String yaml : new String[] { invalid, valid + invalid,
invalid + valid }) {
+ assertThat(validator.validate(yaml)).as("missing required
field: %s", yaml).isNotEmpty();
+ }
+ }
+ }
+
+ private static String booleanQuestion(String value) {
+ return """
+ - semantic:
+ question:
+ actionable:
+ type: boolean
+ instructions: Is this actionable?
+ threshold: %s
+ uncertainty: %s
+ """.formatted(value, value);
+ }
+
@ParameterizedTest
@ValueSource(booleans = { false, true })
void questionTypesHaveDistinctClosedShapes(boolean canonical) throws
Exception {
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorScalarLeniencyTest.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorScalarLeniencyTest.java
index 29e41035a10d..dd774fb45e07 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorScalarLeniencyTest.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorScalarLeniencyTest.java
@@ -18,6 +18,8 @@ package org.apache.camel.dsl.yaml.validator;
import java.util.List;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.Error;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -156,6 +158,38 @@ public class YamlValidatorScalarLeniencyTest {
""", "array expected");
}
+ @Test
+ public void testPublishedSchemasMakeRestParameterTypeOptional() throws
Exception {
+ for (String resource : List.of("/schema/camelYamlDsl.json",
"/schema/camelYamlDsl-canonical.json")) {
+ try (var stream =
YamlValidator.class.getResourceAsStream(resource)) {
+ var parameter = new ObjectMapper().readTree(stream)
+
.at("/items/definitions/org.apache.camel.model.rest.ParamDefinition");
+ assertThat(parameter.path("required")).as(resource)
+ .extracting(JsonNode::asText).containsExactly("name");
+
assertThat(parameter.at("/properties/type/default").asText()).isEqualTo("path");
+ }
+ }
+ }
+
+ @Test
+ public void testRestParameterDefaultsItsTypeButStillRequiresItsName()
throws Exception {
+ String yaml = """
+ - rest:
+ path: /orders
+ get:
+ - path: /{id}
+ param:
+ - name: id
+ to:
+ uri: direct:lookup
+ """;
+ for (YamlValidator validator : List.of(classic, canonical)) {
+ assertThat(validator.validate(yaml)).isEmpty();
+ assertThat(validator.validate(yaml.replace("name: id", "type:
path"))).isNotEmpty();
+ assertThat(validator.validate(yaml.replace("name: id", "name: id\n
type: invalid"))).isNotEmpty();
+ }
+ }
+
private void assertRejectedYaml(String yaml, String expectedInMessage) {
for (YamlValidator validator : List.of(classic, canonical)) {
String mode = validator.isCanonical() ? "canonical" : "classic";
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
index 3332a147530a..2d6f52330f59 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
@@ -11924,7 +11924,7 @@
"enum" : [ "body", "formData", "header", "path", "query" ]
}
},
- "required" : [ "name", "type" ]
+ "required" : [ "name" ]
},
"org.apache.camel.model.rest.PatchDefinition" : {
"title" : "Patch",
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
index 23600b53d7dd..79ce18977038 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
@@ -17887,7 +17887,6 @@
"description" : "Sets the parameter type such as body, form, header,
path, or query.",
"title" : "Type",
"default" : "path",
- "required" : true,
"displayName" : "Type",
"kind" : "attribute",
"index" : 2,
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
index 10dc709a91b8..31a46e9a5ee5 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
@@ -14408,7 +14408,7 @@
"enum" : [ "body", "formData", "header", "path", "query" ]
}
},
- "required" : [ "name", "type" ]
+ "required" : [ "name" ]
},
"org.apache.camel.model.rest.PatchDefinition" : {
"title" : "Patch",
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/YamlCompletionTreeTest.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/YamlCompletionTreeTest.java
index 36d10c2916ba..49b519993a4f 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/YamlCompletionTreeTest.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/YamlCompletionTreeTest.java
@@ -129,6 +129,16 @@ class YamlCompletionTreeTest {
assertThat(exprChild.get("kind").asText()).isEqualTo("value");
}
+ @Test
+ void restParameterTypeDefaultsToPathAndIsOptional() {
+ JsonNode parameter = nodes.get("param");
+ JsonNode type = findChild(parameter, "type");
+ assertThat(type).isNotNull();
+ assertThat(type.path("default").asText()).isEqualTo("path");
+ assertThat(type.path("required").asBoolean()).isFalse();
+ assertThat(findChild(parameter,
"name").path("required").asBoolean()).isTrue();
+ }
+
@Test
void beanEipMetadataIsNotOverwrittenByBeanLanguage() {
JsonNode bean = nodes.get("bean");