This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-24694
in repository https://gitbox.apache.org/repos/asf/camel.git

commit e1d223d3c3a791e81e44a392b757bacdec19726d
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Sep 12 11:08:53 2026 +0200

    CAMEL-24694: camel-yaml-dsl - validator must accept the scalar forms the 
runtime accepts
    
    The generated schema types scalar attributes from the model metadata, but 
the
    model fields are String and the runtime converts the text (resolving 
property
    placeholders) when the route starts. Every schema consumer - camel validate
    yaml, the TUI save-time validation and its MCP tools, camel-jbang-mcp, IDE
    plugins and Kaoto - therefore refused input that camel run accepts.
    
    The schema keeps its real types, since Kaoto forms, TUI completion and 
catalog
    docs rely on them. The leniency goes in YamlValidator instead, in both 
classic
    and canonical mode:
    
    * typeLoose(true) accepts a quoted scalar that parses as the expected type
      (parallelProcessing: "true"); "yes please" is still rejected.
    * isRuntimeAcceptedScalar drops a type error when the value is a property
      placeholder at a typed attribute, or a number/boolean at a string-typed
      attribute such as a duration.
    
    For the validator to stay honest the runtime has to defer the conversion for
    every scalar attribute the schema exposes. Two were still converted while
    deserializing, so a property placeholder silently became false:
    
    * ParamDefinition.required
    * RouteTemplateParameterDefinition.required
    
    Both are now String fields with @Metadata(javaType) like the rest of the 
model,
    so the schema still emits boolean while the runtime resolves the 
placeholder.
    
    The three attributes that are still converted eagerly (inheritErrorHandler 
on
    circuitBreaker and failover, and BeanConstructorDefinition.index) are not
    reachable from the generated schema, so the leniency cannot affect them. 
They
    are tracked in CAMEL-24696.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/catalog/schemas/camel-spring.xsd  |   4 +-
 .../apache/camel/catalog/schemas/camel-xml-io.xsd  |   4 +-
 .../apache/camel/openapi/RestOpenApiReader.java    |  15 ++-
 .../java/org/apache/camel/impl/DefaultModel.java   |  11 +-
 .../camel/model/RouteTemplateDefinition.java       |   2 +-
 .../model/RouteTemplateParameterDefinition.java    |  15 ++-
 .../apache/camel/model/rest/ParamDefinition.java   |  20 ++-
 .../apache/camel/model/rest/RestDefinition.java    |   2 +-
 .../apache/camel/builder/RouteTemplateTest.java    |  41 ++++++
 .../camel/component/rest/FromRestGetTest.java      |   4 +-
 .../apache/camel/java/out/JavaDslModelWriter.java  |   4 +-
 .../camel/java/out/JavaDslModelWriterSupport.java  |   2 +-
 .../java/org/apache/camel/xml/in/ModelParser.java  |   4 +-
 .../java/org/apache/camel/xml/out/ModelWriter.java |   4 +-
 .../org/apache/camel/yaml/out/YamlModelWriter.java |   4 +-
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  29 +++++
 .../dsl/yaml/deserializers/ModelDeserializers.java |   4 +-
 .../camel/dsl/yaml/validator/YamlValidator.java    |  66 +++++++++-
 .../validator/YamlValidatorScalarLeniencyTest.java | 139 +++++++++++++++++++++
 .../camel/dsl/yaml/KameletRoutesBuilderLoader.java |   2 +-
 20 files changed, 342 insertions(+), 34 deletions(-)

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 74709394fc46..5ee9404250ed 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
@@ -12863,7 +12863,7 @@ The name of the template parameter.
         </xs:documentation>
       </xs:annotation>
     </xs:attribute>
-    <xs:attribute name="required" type="xs:boolean">
+    <xs:attribute name="required" type="xs:string">
       <xs:annotation>
         <xs:documentation xml:lang="en">
 <![CDATA[
@@ -15993,7 +15993,7 @@ The default value of the parameter.
         </xs:documentation>
       </xs:annotation>
     </xs:attribute>
-    <xs:attribute name="required" type="xs:boolean">
+    <xs:attribute name="required" type="xs:string">
       <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 c0902dd541fe..dbbcbd1ba490 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
@@ -12006,7 +12006,7 @@ The name of the template parameter.
         </xs:documentation>
       </xs:annotation>
     </xs:attribute>
-    <xs:attribute name="required" type="xs:boolean">
+    <xs:attribute name="required" type="xs:string">
       <xs:annotation>
         <xs:documentation xml:lang="en">
 <![CDATA[
@@ -15136,7 +15136,7 @@ The default value of the parameter.
         </xs:documentation>
       </xs:annotation>
     </xs:attribute>
-    <xs:attribute name="required" type="xs:boolean">
+    <xs:attribute name="required" type="xs:string">
       <xs:annotation>
         <xs:documentation xml:lang="en">
 <![CDATA[
diff --git 
a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java
 
b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java
index 13ff8f7a4091..2f05c6f6d303 100644
--- 
a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java
+++ 
b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java
@@ -121,6 +121,15 @@ public class RestOpenApiReader {
         return camelContext.resolvePropertyPlaceholders(text);
     }
 
+    /**
+     * The required flag of a rest parameter. It is a String on the model so 
that a property placeholder can be used,
+     * and is resolved here.
+     */
+    private static boolean isRequired(CamelContext camelContext, 
ParamDefinition param) {
+        Boolean required = CamelContextHelper.parseBoolean(camelContext, 
param.getRequired());
+        return required != null && required;
+    }
+
     private static List<String> getValue(CamelContext camelContext, 
List<String> list) {
         if (list == null) {
             return null;
@@ -565,7 +574,7 @@ public class RestOpenApiReader {
                 if 
(org.apache.camel.util.ObjectHelper.isNotEmpty(param.getDescription())) {
                     parameter.setDescription(getValue(camelContext, 
param.getDescription()));
                 }
-                parameter.setRequired(param.getRequired());
+                parameter.setRequired(isRequired(camelContext, param));
 
                 final String dataType
                         = getValue(camelContext, param.getDataType() != null ? 
param.getDataType() : "string");
@@ -660,7 +669,7 @@ public class RestOpenApiReader {
                 // In OpenAPI 3x, body parameters are replaced by requestBody
                 if (parameter.getIn().equals("body")) {
                     RequestBody reqBody = new RequestBody().content(new 
Content());
-                    reqBody.setRequired(param.getRequired());
+                    reqBody.setRequired(isRequired(camelContext, param));
                     reqBody.setDescription(getValue(camelContext, 
param.getDescription()));
                     op.setRequestBody(reqBody);
                     String type = getValue(camelContext, verb.getType());
@@ -727,7 +736,7 @@ public class RestOpenApiReader {
                     fieldSchema.setDefault(getValue(camelContext, 
param.getDefaultValue()));
                 }
                 formSchema.addProperty(name, fieldSchema);
-                if (param.getRequired()) {
+                if (isRequired(camelContext, param)) {
                     requiredFields.add(name);
                 }
             }
diff --git 
a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java 
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java
index 8419f9f61992..1fbf9a703805 100644
--- 
a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java
+++ 
b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java
@@ -557,7 +557,7 @@ public class DefaultModel implements Model {
                 } else if (temp.getDefaultValue() != null) {
                     addProperty(prop, temp.getName(), temp.getDefaultValue());
                     addProperty(propDefaultValues, temp.getName(), 
temp.getDefaultValue());
-                } else if (temp.isRequired() && 
!routeTemplateContext.hasParameter(temp.getName())) {
+                } else if (isTemplateParameterRequired(temp) && 
!routeTemplateContext.hasParameter(temp.getName())) {
                     // this is a required parameter which is missing
                     missingParameters.add(temp.getName());
                 }
@@ -654,6 +654,15 @@ public class DefaultModel implements Model {
         return def.getId();
     }
 
+    /**
+     * Whether the given route template parameter is required. The attribute 
is a String so that a property placeholder
+     * can be used, and it is assumed to be required unless explicitly set to 
false.
+     */
+    private boolean 
isTemplateParameterRequired(RouteTemplateParameterDefinition temp) {
+        Boolean required = CamelContextHelper.parseBoolean(camelContext, 
temp.getRequired());
+        return required == null || required;
+    }
+
     private static void addProperty(Map<String, Object> prop, String key, 
Object value) {
         prop.put(key, value);
         // support also camelCase and kebab-case because route templates 
(kamelets)
diff --git 
a/core/camel-core-model/src/main/java/org/apache/camel/model/RouteTemplateDefinition.java
 
b/core/camel-core-model/src/main/java/org/apache/camel/model/RouteTemplateDefinition.java
index 44baa45e6dc3..579b96fefe67 100644
--- 
a/core/camel-core-model/src/main/java/org/apache/camel/model/RouteTemplateDefinition.java
+++ 
b/core/camel-core-model/src/main/java/org/apache/camel/model/RouteTemplateDefinition.java
@@ -418,7 +418,7 @@ public class RouteTemplateDefinition extends 
OptionalIdentifiedDefinition<RouteT
             this.templateParameters = new ArrayList<>();
         }
         RouteTemplateParameterDefinition def = new 
RouteTemplateParameterDefinition(name, null, description);
-        def.setRequired(false);
+        def.setRequired("false");
         this.templateParameters.add(def);
     }
 
diff --git 
a/core/camel-core-model/src/main/java/org/apache/camel/model/RouteTemplateParameterDefinition.java
 
b/core/camel-core-model/src/main/java/org/apache/camel/model/RouteTemplateParameterDefinition.java
index 5fc40fccab36..ccf09be6e337 100644
--- 
a/core/camel-core-model/src/main/java/org/apache/camel/model/RouteTemplateParameterDefinition.java
+++ 
b/core/camel-core-model/src/main/java/org/apache/camel/model/RouteTemplateParameterDefinition.java
@@ -38,7 +38,7 @@ public class RouteTemplateParameterDefinition {
     @XmlAttribute
     @Metadata(description = "Whether this template parameter is required. A 
required parameter must have a value provided when creating a route from the 
template.",
               javaType = "java.lang.Boolean")
-    Boolean required;
+    String required;
     @XmlAttribute
     @Metadata(description = "The default value of the template parameter. Used 
when no explicit value is provided when creating a route from the template.")
     String defaultValue;
@@ -55,9 +55,16 @@ public class RouteTemplateParameterDefinition {
         this.defaultValue = defaultValue;
     }
 
+    /**
+     * Whether this template parameter is required, assuming required unless 
explicitly set to false.
+     * <p>
+     * Note that this does not resolve property placeholders; use
+     * {@code CamelContextHelper.parseBoolean(camelContext, getRequired())} 
where a
+     * {@link org.apache.camel.CamelContext} is available.
+     */
     public boolean isRequired() {
         // assumed to be required if not set explicit to false
-        return required == null || required;
+        return required == null || !"false".equalsIgnoreCase(required);
     }
 
     public String getName() {
@@ -68,11 +75,11 @@ public class RouteTemplateParameterDefinition {
         this.name = name;
     }
 
-    public Boolean getRequired() {
+    public String getRequired() {
         return required;
     }
 
-    public void setRequired(Boolean required) {
+    public void setRequired(String required) {
         this.required = required;
     }
 
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 68607024c1b6..54c8f04eb85b 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
@@ -59,9 +59,9 @@ public class ParamDefinition {
     @Metadata(description = "The default value of the parameter.")
     private String defaultValue;
     @XmlAttribute
-    @Metadata(description = "Sets the parameter required flag.",
+    @Metadata(description = "Sets the parameter required flag.", javaType = 
"java.lang.Boolean",
               defaultValue = "true")
-    private Boolean required;
+    private String required;
     @XmlAttribute
     @Metadata(description = "Sets the parameter collection format.",
               defaultValue = "csv")
@@ -125,11 +125,11 @@ public class ParamDefinition {
         this.defaultValue = defaultValue;
     }
 
-    public Boolean getRequired() {
-        return required != null ? required : true;
+    public String getRequired() {
+        return required != null ? required : "true";
     }
 
-    public void setRequired(Boolean required) {
+    public void setRequired(String required) {
         this.required = required;
     }
 
@@ -211,6 +211,16 @@ public class ParamDefinition {
      * Whether the parameter is required
      */
     public ParamDefinition required(Boolean required) {
+        setRequired(required != null ? required.toString() : null);
+        return this;
+    }
+
+    /**
+     * Whether the parameter is required.
+     * <p>
+     * The value can be a property placeholder, which is resolved when the 
route starts.
+     */
+    public ParamDefinition required(String required) {
         setRequired(required);
         return this;
     }
diff --git 
a/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestDefinition.java
 
b/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestDefinition.java
index d3e48812684d..5b3163f48d20 100644
--- 
a/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestDefinition.java
+++ 
b/core/camel-core-model/src/main/java/org/apache/camel/model/rest/RestDefinition.java
@@ -1201,7 +1201,7 @@ public class RestDefinition extends 
OptionalIdentifiedDefinition<RestDefinition>
                     binding.addAllowedValue(name, parseText(camelContext, 
param.getAllowableValuesAsCommaString()));
                 }
                 // register which parameters are required
-                Boolean required = param.getRequired();
+                Boolean required = parseBoolean(camelContext, 
param.getRequired());
                 if (required != null && required) {
                     if (RestParamType.query == type) {
                         binding.addRequiredQueryParameter(name);
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/builder/RouteTemplateTest.java 
b/core/camel-core/src/test/java/org/apache/camel/builder/RouteTemplateTest.java
index 81454f8ee875..7291d9391c4b 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/builder/RouteTemplateTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/builder/RouteTemplateTest.java
@@ -168,6 +168,47 @@ public class RouteTemplateTest extends ContextTestSupport {
         assertMockEndpointsSatisfied();
     }
 
+    @Test
+    public void testRequiredTemplateParameterSupportsPropertyPlaceholder() {
+        // the required attribute is a String on the model so it can be a 
property placeholder, which is
+        // resolved when the route is created from the template. Before 
CAMEL-24694 the text was converted
+        // while loading, so Boolean.valueOf("{{barRequired}}") silently 
yielded false and turned a required
+        // parameter into an optional one.
+        context.getPropertiesComponent().addInitialProperty("barRequired", 
"true");
+
+        RouteTemplateDefinition routeTemplate = 
context.getRouteTemplateDefinition("myTemplate");
+        
routeTemplate.getTemplateParameters().get(1).setRequired("{{barRequired}}");
+
+        Map<String, Object> parameters = new HashMap<>();
+        parameters.put("foo", "one");
+
+        IllegalArgumentException e = 
assertThrows(IllegalArgumentException.class,
+                () -> context.addRouteFromTemplate(null, "myTemplate", 
parameters),
+                "Should throw exception");
+
+        assertEquals("Route template myTemplate the following mandatory 
parameters must be provided: bar", e.getMessage());
+    }
+
+    @Test
+    public void testOptionalTemplateParameterSupportsPropertyPlaceholder() 
throws Exception {
+        context.getPropertiesComponent().addInitialProperty("barRequired", 
"false");
+
+        RouteTemplateDefinition routeTemplate = 
context.getRouteTemplateDefinition("myTemplate");
+        
routeTemplate.getTemplateParameters().get(1).setRequired("{{barRequired}}");
+
+        getMockEndpoint("mock:cheese").expectedBodiesReceived("Hello Cheese");
+
+        // bar is not required, so it may be supplied without being mandatory
+        Map<String, Object> parameters = new HashMap<>();
+        parameters.put("foo", "one");
+        parameters.put("bar", "cheese");
+        context.addRouteFromTemplate("first", "myTemplate", parameters);
+
+        template.sendBody("direct:one", "Hello Cheese");
+
+        assertMockEndpointsSatisfied();
+    }
+
     @Test
     public void testCreateRouteFromRouteTemplateMissingParameter() throws 
Exception {
         assertEquals(1, context.getRouteTemplateDefinitions().size());
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetTest.java
index 4e56dd864737..85297a072d4f 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/rest/FromRestGetTest.java
@@ -89,8 +89,8 @@ public class FromRestGetTest extends ContextTestSupport {
 
         assertEquals("header_count", 
rest.getVerbs().get(0).getParams().get(0).getName());
         assertEquals("header_letter", 
rest.getVerbs().get(0).getParams().get(1).getName());
-        assertEquals(Boolean.TRUE, 
rest.getVerbs().get(0).getParams().get(0).getRequired());
-        assertEquals(Boolean.FALSE, 
rest.getVerbs().get(0).getParams().get(1).getRequired());
+        assertEquals("true", 
rest.getVerbs().get(0).getParams().get(0).getRequired());
+        assertEquals("false", 
rest.getVerbs().get(0).getParams().get(1).getRequired());
 
         assertEquals("300", 
rest.getVerbs().get(0).getResponseMsgs().get(0).getCode());
         assertEquals("rate", 
rest.getVerbs().get(0).getResponseMsgs().get(0).getHeaders().get(0).getName());
diff --git 
a/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java
 
b/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java
index 7c67c5722ef3..14d9a9bde751 100644
--- 
a/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java
+++ 
b/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java
@@ -2471,7 +2471,7 @@ public class JavaDslModelWriter extends 
JavaDslModelWriterSupport {
     protected void doWriteRouteTemplateParameterDefinition(StringBuilder sb, 
RouteTemplateParameterDefinition def) {
         doWriteAttribute(sb, "description", def.getDescription(), null);
         doWriteAttribute(sb, "name", def.getName(), null);
-        doWriteAttribute(sb, "required", toString(def.getRequired()), null);
+        doWriteAttribute(sb, "required", def.getRequired(), null);
         doWriteAttribute(sb, "defaultValue", def.getDefaultValue(), null);
     }
     protected void doWriteRouteTemplatesDefinition(StringBuilder sb, 
RouteTemplatesDefinition def) {
@@ -3754,7 +3754,7 @@ public class JavaDslModelWriter extends 
JavaDslModelWriterSupport {
         doWriteAttribute(sb, "name", def.getName(), null);
         doWriteAttribute(sb, "type", toString(def.getType()), "path");
         doWriteAttribute(sb, "defaultValue", def.getDefaultValue(), null);
-        doWriteAttribute(sb, "required", toString(def.getRequired()), "true");
+        doWriteAttribute(sb, "required", def.getRequired(), "true");
         doWriteAttribute(sb, "collectionFormat", 
toString(def.getCollectionFormat()), "csv");
         doWriteAttribute(sb, "arrayType", def.getArrayType(), "string");
         doWriteAttribute(sb, "dataType", def.getDataType(), "string");
diff --git 
a/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java
 
b/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java
index 95ba89355078..4912d1d8c09e 100644
--- 
a/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java
+++ 
b/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java
@@ -429,7 +429,7 @@ public abstract class JavaDslModelWriterSupport {
     private void writeTemplateParameter(StringBuilder sb, 
RouteTemplateParameterDefinition param) {
         boolean hasDefault = param.getDefaultValue() != null;
         boolean hasDescription = param.getDescription() != null;
-        boolean isOptional = Boolean.FALSE.equals(param.getRequired());
+        boolean isOptional = "false".equalsIgnoreCase(param.getRequired());
 
         if (isOptional && !hasDefault) {
             
sb.append(NL).append(indent()).append(".templateOptionalParameter(").append(quote(param.getName()));
diff --git 
a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java 
b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java
index 009c61821024..9393502a94eb 100644
--- 
a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java
+++ 
b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java
@@ -1014,7 +1014,7 @@ public class ModelParser extends BaseParser {
                 case "defaultValue": def.setDefaultValue(val); yield true;
                 case "description": def.setDescription(val); yield true;
                 case "name": def.setName(val); yield true;
-                case "required": def.setRequired(Boolean.valueOf(val)); yield 
true;
+                case "required": def.setRequired(val); yield true;
                 default: yield false;
             }, noElementHandler(), noValueHandler());
     }
@@ -2531,7 +2531,7 @@ public class ModelParser extends BaseParser {
                 case "defaultValue": def.setDefaultValue(val); yield true;
                 case "description": def.setDescription(val); yield true;
                 case "name": def.setName(val); yield true;
-                case "required": def.setRequired(Boolean.valueOf(val)); yield 
true;
+                case "required": def.setRequired(val); yield true;
                 case "type": def.setType(RestParamType.valueOf(val)); yield 
true;
                 default: yield false;
             }, (def, key) -> switch (key) {
diff --git 
a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java
 
b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java
index d9636653097c..4fd5f78262cb 100644
--- 
a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java
+++ 
b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java
@@ -1674,7 +1674,7 @@ public class ModelWriter extends BaseWriter {
         startElement(name);
         doWriteAttribute("description", def.getDescription(), null);
         doWriteAttribute("name", def.getName(), null);
-        doWriteAttribute("required", toString(def.getRequired()), null);
+        doWriteAttribute("required", def.getRequired(), null);
         doWriteAttribute("defaultValue", def.getDefaultValue(), null);
         endElement(name);
     }
@@ -3275,7 +3275,7 @@ public class ModelWriter extends BaseWriter {
         doWriteAttribute("name", def.getName(), null);
         doWriteAttribute("type", toString(def.getType()), "path");
         doWriteAttribute("defaultValue", def.getDefaultValue(), null);
-        doWriteAttribute("required", toString(def.getRequired()), "true");
+        doWriteAttribute("required", def.getRequired(), "true");
         doWriteAttribute("collectionFormat", 
toString(def.getCollectionFormat()), "csv");
         doWriteAttribute("arrayType", def.getArrayType(), "string");
         doWriteAttribute("dataType", def.getDataType(), "string");
diff --git 
a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java
 
b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java
index 9a2fdee0b5e0..af8cd632617c 100644
--- 
a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java
+++ 
b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java
@@ -1670,7 +1670,7 @@ public class YamlModelWriter extends 
YamlModelWriterSupport {
         JsonObject jo = new JsonObject();
         doWriteAttribute(jo, "description", def.getDescription(), null);
         doWriteAttribute(jo, "name", def.getName(), null);
-        doWriteAttribute(jo, "required", toString(def.getRequired()), null);
+        doWriteAttribute(jo, "required", def.getRequired(), null);
         doWriteAttribute(jo, "defaultValue", def.getDefaultValue(), null);
         return jo;
     }
@@ -3273,7 +3273,7 @@ public class YamlModelWriter extends 
YamlModelWriterSupport {
         doWriteAttribute(jo, "name", def.getName(), null);
         doWriteAttribute(jo, "type", toString(def.getType()), "path");
         doWriteAttribute(jo, "defaultValue", def.getDefaultValue(), null);
-        doWriteAttribute(jo, "required", toString(def.getRequired()), "true");
+        doWriteAttribute(jo, "required", def.getRequired(), "true");
         doWriteAttribute(jo, "collectionFormat", 
toString(def.getCollectionFormat()), "csv");
         doWriteAttribute(jo, "arrayType", def.getArrayType(), "string");
         doWriteAttribute(jo, "dataType", def.getDataType(), "string");
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 68aa3ebbb47f..21a6fd92ea74 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -1890,3 +1890,32 @@ Documents carrying an internal DTD subset still parse.
 To restore the previous behaviour and allow external entity resolution, set 
the new
 `allowExternalEntities` option to `true` on the data format or on the endpoint
 (`smooks:config.xml?allowExternalEntities=true`).
+
+=== camel-core - the required attribute on rest param and route template 
parameter is now a String
+
+`ParamDefinition.required` (the rest DSL `param`) and 
`RouteTemplateParameterDefinition.required`
+(the `templateParameter` of a route template) are now declared as `String` 
instead of `Boolean`, the
+same way nearly every other scalar attribute in the Camel model is declared. 
This allows a property
+placeholder to be used, which is resolved when the route starts:
+
+[source,yaml]
+----
+- route:
+    templateParameters:
+      - name: greeting
+        required: "{{myRequiredFlag}}"
+----
+
+Before this change the value was converted to a `Boolean` while the route was 
being loaded, so
+`required: "{{myRequiredFlag}}"` silently evaluated to `false` instead of 
resolving the placeholder.
+
+The accessors changed accordingly:
+
+* `ParamDefinition.getRequired()` and `setRequired(...)` now use `String` 
instead of `Boolean`.
+* `RouteTemplateParameterDefinition.getRequired()` and `setRequired(...)` now 
use `String` instead of
+  `Boolean`. `isRequired()` is unchanged and still returns `boolean`, but it 
does not resolve property
+  placeholders — use `CamelContextHelper.parseBoolean(camelContext, 
getRequired())` where a
+  `CamelContext` is available.
+
+The fluent builder `ParamDefinition.required(Boolean)` is unchanged, and a 
`required(String)` overload
+was added for placeholders. Routes written in XML, YAML or the Java DSL do not 
need any change.
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 afc9760da112..a39a89bf599b 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
@@ -11610,7 +11610,7 @@ public final class ModelDeserializers extends 
YamlDeserializerSupport {
                 }
                 case "required": {
                     String val = asText(node);
-                    target.setRequired(java.lang.Boolean.valueOf(val));
+                    target.setRequired(val);
                     break;
                 }
                 case "type": {
@@ -15771,7 +15771,7 @@ public final class ModelDeserializers extends 
YamlDeserializerSupport {
                 }
                 case "required": {
                     String val = asText(node);
-                    target.setRequired(java.lang.Boolean.valueOf(val));
+                    target.setRequired(val);
                     break;
                 }
                 default: {
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 5b10bcc80123..18885aed2e9f 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
@@ -117,6 +117,7 @@ public class YamlValidator {
 
     private List<Error> validate(JsonNode target) {
         var errors = filterOneOfNoise(new 
ArrayList<>(schema.validate(target)));
+        errors.removeIf(YamlValidator::isRuntimeAcceptedScalar);
         if (canonical) {
             checkOneOfCardinality(target, new NodePath(PathType.JSON_POINTER), 
errors);
         }
@@ -336,6 +337,65 @@ public class YamlValidator {
         return groups;
     }
 
+    /**
+     * Whether the schema rejected a scalar that the Camel runtime accepts, in 
which case the error is dropped.
+     * <p>
+     * Camel's model declares nearly every scalar attribute as a {@code 
String} field carrying the real type in
+     * {@code @Metadata(javaType = ...)}, so that property placeholders can be 
used and the text is converted when the
+     * route starts. The generated schema keeps the real type because tooling 
(Kaoto forms, TUI completion, catalog
+     * docs) relies on it, which makes the schema stricter than the runtime in 
two ways:
+     * <ul>
+     * <li>a property placeholder at a typed attribute - the runtime resolves 
it before converting;</li>
+     * <li>a number or boolean at a string-typed attribute (e.g. a {@code 
duration}) - the runtime converts any scalar
+     * to text.</li>
+     * </ul>
+     * Quoted scalars that parse as the expected type are already handled by 
the registry's type-loose mode, see
+     * {@link #init()}. Everything else stays strict: unknown properties, 
structure, enums, and strings that do not
+     * parse as the expected type.
+     * <p>
+     * This assumes the runtime defers the conversion for every scalar 
attribute the schema exposes. The few model
+     * attributes that are still converted while deserializing (so a 
placeholder is never resolved for them) are not
+     * reachable from the schema today - see CAMEL-24696 before exposing one 
of them.
+     */
+    static boolean isRuntimeAcceptedScalar(Error error) {
+        if (!"type".equals(error.getKeyword())) {
+            return false;
+        }
+        JsonNode instance = error.getInstanceNode();
+        if (instance == null) {
+            return false;
+        }
+        if (instance.isTextual()) {
+            return hasPropertyPlaceholder(instance.asText());
+        }
+        // the runtime converts any scalar to text, so a number or boolean is 
fine wherever a string is expected
+        return (instance.isNumber() || instance.isBoolean()) && 
isExpectedType(error, "string");
+    }
+
+    private static boolean hasPropertyPlaceholder(String text) {
+        int start = text.indexOf("{{");
+        return start >= 0 && text.indexOf("}}", start + 2) > start;
+    }
+
+    private static boolean isExpectedType(Error error, String type) {
+        JsonNode schemaNode = error.getSchemaNode();
+        if (schemaNode == null) {
+            return false;
+        }
+        if (schemaNode.isTextual()) {
+            return type.equals(schemaNode.asText());
+        }
+        // "type" may also be declared as an array of accepted types
+        if (schemaNode.isArray()) {
+            for (JsonNode t : schemaNode) {
+                if (type.equals(t.asText())) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     private static Error parseError(Exception e) {
         String msg = e.getClass().getName() + ": " + e.getMessage();
         return Error.builder()
@@ -349,7 +409,11 @@ public class YamlValidator {
         String location = canonical ? LOCATION_CANONICAL : LOCATION;
         var model = 
mapper.readTree(YamlValidator.class.getResourceAsStream(location));
         var version = 
getSpecificationVersion(model).orElse(SpecificationVersion.DRAFT_4);
-        var config = 
SchemaRegistryConfig.builder().locale(Locale.ENGLISH).build();
+        // typeLoose lets a quoted scalar that parses as the expected type 
validate (e.g. parallelProcessing: "true"
+        // at a boolean attribute). Camel's runtime accepts it because the 
model field is a String, so the schema
+        // would otherwise be stricter than the runtime. Values that do not 
parse (e.g. "yes please") are still
+        // rejected. See isRuntimeAcceptedScalar for the cases typeLoose does 
not cover.
+        var config = 
SchemaRegistryConfig.builder().locale(Locale.ENGLISH).typeLoose(true).build();
 
         // Register "deprecated" as a known non-validation keyword to suppress 
warnings
         Dialect base = getBaseDialect(version);
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
new file mode 100644
index 000000000000..0be66e4b5ee3
--- /dev/null
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorScalarLeniencyTest.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.yaml.validator;
+
+import java.util.List;
+
+import com.networknt.schema.Error;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-24694: the generated schema types scalar attributes from the model 
metadata, but the model fields are String
+ * and the runtime converts (and resolves property placeholders) when the 
route starts. The validator must therefore
+ * accept the string forms the runtime accepts, in both classic and canonical 
mode, while staying strict about values
+ * the runtime would genuinely reject.
+ */
+public class YamlValidatorScalarLeniencyTest {
+
+    private static YamlValidator classic;
+    private static YamlValidator canonical;
+
+    @BeforeAll
+    public static void setup() throws Exception {
+        classic = new YamlValidator();
+        classic.init();
+        canonical = new YamlValidator(true);
+        canonical.init();
+    }
+
+    @Test
+    public void testQuotedBooleanAccepted() {
+        assertAccepted("split", "parallelProcessing: \"true\"");
+    }
+
+    @Test
+    public void testNativeBooleanStillAccepted() {
+        assertAccepted("split", "parallelProcessing: true");
+    }
+
+    @Test
+    public void testPlaceholderAtBooleanAccepted() {
+        assertAccepted("split", "parallelProcessing: \"{{myParallel}}\"");
+    }
+
+    @Test
+    public void testQuotedIntegerAccepted() {
+        assertAccepted("split", "group: \"100\"");
+    }
+
+    @Test
+    public void testPlaceholderAtIntegerAccepted() {
+        assertAccepted("split", "group: \"{{myGroup}}\"");
+    }
+
+    @Test
+    public void testQuotedNumberAccepted() {
+        assertAccepted("split", "errorThreshold: \"0.5\"");
+    }
+
+    @Test
+    public void testIntegerAtDurationAccepted() {
+        // timeout is a duration, which the schema emits as string - the 
runtime converts any scalar to text
+        assertAccepted("split", "timeout: 5000");
+    }
+
+    @Test
+    public void testPlaceholderAtDurationAccepted() {
+        assertAccepted("split", "timeout: \"{{myTimeout}}\"");
+    }
+
+    @Test
+    public void testUnparsableStringAtBooleanStillRejected() {
+        assertRejected("split", "parallelProcessing: \"yes please\"", "boolean 
expected");
+    }
+
+    @Test
+    public void testUnparsableStringAtIntegerStillRejected() {
+        assertRejected("split", "group: \"a lot\"", "number expected");
+    }
+
+    @Test
+    public void testUnknownPropertyStillRejected() {
+        assertRejected("split", "cheese: true", "cheese");
+    }
+
+    private void assertAccepted(String eip, String attribute) {
+        for (YamlValidator validator : List.of(classic, canonical)) {
+            String mode = validator.isCanonical() ? "canonical" : "classic";
+            assertThat(validate(validator, eip, attribute))
+                    .as("%s: '%s' is accepted by the runtime and must validate 
in %s mode", eip, attribute, mode)
+                    .isEmpty();
+        }
+    }
+
+    private void assertRejected(String eip, String attribute, String 
expectedInMessage) {
+        for (YamlValidator validator : List.of(classic, canonical)) {
+            String mode = validator.isCanonical() ? "canonical" : "classic";
+            assertThat(validate(validator, eip, attribute))
+                    .as("%s: '%s' must still be rejected in %s mode", eip, 
attribute, mode)
+                    .isNotEmpty()
+                    .anyMatch(e -> e.getMessage().contains(expectedInMessage));
+        }
+    }
+
+    private List<Error> validate(YamlValidator validator, String eip, String 
attribute) {
+        String yaml = """
+                - route:
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - %s:
+                            %s
+                            steps:
+                              - log:
+                                  message: "${body}"
+                """.formatted(eip, attribute);
+        try {
+            return validator.validate(yaml);
+        } catch (Exception e) {
+            throw new AssertionError("Failed to validate:\n" + yaml, e);
+        }
+    }
+}
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/KameletRoutesBuilderLoader.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/KameletRoutesBuilderLoader.java
index 50b0aa75c6e6..d15bb4a257fc 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/KameletRoutesBuilderLoader.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/KameletRoutesBuilderLoader.java
@@ -84,7 +84,7 @@ public class KameletRoutesBuilderLoader extends 
YamlRoutesBuilderLoaderSupport {
                 RouteTemplateParameterDefinition rtpd = new 
RouteTemplateParameterDefinition();
                 rtpd.setName(key);
                 rtpd.setDefaultValue(asText(def));
-                rtpd.setRequired(required.contains(key));
+                rtpd.setRequired(Boolean.toString(required.contains(key)));
                 rtd.getTemplateParameters().add(rtpd);
             }
         }

Reply via email to