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");

Reply via email to