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

davsclaus pushed a commit to branch feature/CAMEL-24711-validate-camel-version
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 7df12d9214faf0c401e3ceb95f02b6c675ed6da8
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 16 07:55:41 2026 +0200

    CAMEL-24711: camel-jbang - camel validate yaml and camel validate source 
take --camel-version and --runtime: the catalog of that version and runtime, 
and the YAML DSL schema read from the camel-yaml-dsl jar of that version; a 
component without a Quarkus extension or Spring Boot starter is an error; 
camel_validate_source validates against the schema of camelVersion
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-devtools.adoc   |  34 +++++++
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |   4 +-
 .../dsl/jbang/core/commands/ai/EndpointChecks.java |  41 +++++++-
 .../jbang/core/commands/ai/SourceValidator.java    |  67 ++++++++++++-
 .../camel/dsl/jbang/core/common/CatalogLoader.java |  48 +++++++++
 .../commands/ai/SourceValidatorVersionTest.java    | 107 +++++++++++++++++++++
 .../commands/ai/fakequarkus/components/log.json    |  68 +++++++++++++
 .../commands/ai/fakequarkus/components/timer.json  |  52 ++++++++++
 .../camel-jbang-plugin-validate/pom.xml            |  12 +++
 .../commands/validate/CatalogVersionMixin.java     |  98 +++++++++++++++++++
 .../commands/validate/SourceValidateCommand.java   |  12 ++-
 .../commands/validate/YamlValidateCommand.java     |  11 ++-
 .../validate/ValidateCamelVersionTest.java         | 100 +++++++++++++++++++
 .../src/test/resources/no-extension.yaml           |  21 ++++
 .../src/test/resources/route.yaml                  |  28 ++++++
 .../camel/dsl/yaml/validator/YamlValidator.java    |  24 ++++-
 .../validator/YamlValidatorSchemaDocumentTest.java |  59 ++++++++++++
 17 files changed, 771 insertions(+), 15 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-devtools.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-devtools.adoc
index d8f1bda0b31c..574f18cfc282 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-devtools.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-devtools.adoc
@@ -421,3 +421,37 @@ When all files pass:
 $ camel validate yaml cheese.yaml bad.yaml
 Validation success (files:2)
 ----
+
+`camel validate yaml` checks the YAML DSL schema, then the endpoint URIs, 
Simple expressions and bean references
+against the Camel catalog (`--catalog=false` for the schema only); 
`--canonical` validates against the canonical
+schema and reports the deprecated compact notation. `camel validate source` 
validates any file `camel run` would
+load: YAML routes, `.properties` files (the `camel.*` keys), Java (compiles 
it), XSLT and XML.
+
+=== Validating against another Camel version or runtime
+
+Both commands validate against the CLI's own Camel version. A project that 
runs another version, or Camel
+Spring Boot or Camel Quarkus, is validated against the catalog and the YAML 
DSL schema of that version with
+`--camel-version` and `--runtime`, the same options `camel catalog` and `camel 
doc` take:
+
+[source,bash]
+----
+$ camel validate yaml --camel-version=4.18.0 orders.yaml
+$ camel validate source --runtime=quarkus --quarkus-version=3.30.1 orders.yaml 
application.properties
+Validation error detected (errors:1)
+
+       File: orders.yaml
+               line 4: atmosphere-websocket: Camel Quarkus has no extension 
for this component (no camel-quarkus-atmosphere-websocket); pick a component 
that has one, camel_catalog_find lists them
+----
+
+The catalog of that version is downloaded as for `camel catalog`, and the 
schema is read from the
+`camel-yaml-dsl` jar of that version (`--repos` and `--download=false` apply 
to both). With `--runtime=quarkus`
+the Quarkus platform is resolved from the Camel version through the Quarkus 
extension registry (cached for a
+day under `~/.camel`), or given with `--quarkus-version`; a component of the 
route that has no Camel Quarkus
+extension, or no Camel Spring Boot starter with `--runtime=spring-boot`, is an 
error. `--canonical` needs Camel
+4.22 or later, the first version with the canonical schema.
+
+The Simple expressions are parsed by the CLI's own Simple language (the 
catalog of the other version supplies
+its functions), and the Java, XSLT and XML checks of `camel validate source` 
use the CLI's classpath.
+
+The `camel_validate_source` tool of the MCP servers 
(xref:camel-jbang-mcp.adoc[]) does the same when it is
+given a `camelVersion`: the catalog and the schema of that version.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 3a53d0cacf74..6b272ccadbd8 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -310,7 +310,9 @@ project `directory` as an argument, the runtime tools take 
the integration `name
 | `camel_validate_source`
 | Validates Camel YAML DSL or `.properties` source without writing: the YAML 
DSL schema (a misspelled option
   such as `logLevel` instead of `loggingLevel`), endpoint URIs, simple 
expressions, and `camel.*` options.
-  Takes the content, or reads the file from the project directory.
+  Takes the content, or reads the file from the project directory. With 
`camelVersion` (or the version of the
+  selected integration in the TUI) the catalog and the YAML DSL schema of that 
Camel version answer, the schema
+  read from its `camel-yaml-dsl` jar; without it the CLI's own, with nothing 
to download.
 
 | `camel_get_files`
 | The source files of a project directory (name, size, type), or the content 
of one of them.
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
index f992dd13de86..2ffe970314d1 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
@@ -26,7 +26,9 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.catalog.EndpointValidationResult;
+import org.apache.camel.catalog.RuntimeProvider;
 
 import static 
org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.YAML_URI_PATTERN;
 import static 
org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.countLeadingSpaces;
@@ -179,8 +181,15 @@ final class EndpointChecks {
             try {
                 EndpointValidationResult result
                         = catalog.validateEndpointProperties(fullUri, false, 
consumerOnly, producerOnly);
+                String scheme = fullUri.contains(":") ? fullUri.substring(0, 
fullUri.indexOf(':')) : fullUri;
+                if (result.getUnknownComponent() != null) {
+                    // a warning to the catalog, an error when the runtime is 
what lacks the component
+                    String missing = missingInRuntime(catalog, scheme);
+                    if (missing != null) {
+                        errors.add(linePrefix(i) + missing);
+                    }
+                }
                 if (!result.isSuccess()) {
-                    String scheme = fullUri.contains(":") ? 
fullUri.substring(0, fullUri.indexOf(':')) : fullUri;
                     collectEndpointErrors(errors, result, scheme, i, 
optionLineMap);
                 }
                 checkRegexOptions(errors, fullUri, i, optionLineMap);
@@ -191,6 +200,36 @@ final class EndpointChecks {
         return errors;
     }
 
+    private static volatile CamelCatalog defaultCatalog;
+
+    /**
+     * The message for a component the catalog of a runtime (Camel Quarkus, 
Camel Spring Boot) does not have while Camel
+     * has it: the runtime has no extension or starter for it. Null for the 
default catalog, whose unknown components
+     * are not reported: a project can register a component of its own 
(CAMEL-24711).
+     */
+    static String missingInRuntime(CamelCatalog catalog, String scheme) {
+        RuntimeProvider provider = catalog.getRuntimeProvider();
+        String name = provider != null ? provider.getProviderName() : null;
+        if (name == null || "default".equals(name)) {
+            return null;
+        }
+        CamelCatalog plain = defaultCatalog;
+        if (plain == null) {
+            plain = new DefaultCamelCatalog();
+            defaultCatalog = plain;
+        }
+        if (plain.componentModel(scheme) == null) {
+            return null;
+        }
+        return switch (name) {
+            case "quarkus" -> scheme + ": Camel Quarkus has no extension for 
this component (no camel-quarkus-" + scheme
+                              + "); pick a component that has one, 
camel_catalog_find lists them";
+            case "springboot" -> scheme + ": Camel Spring Boot has no starter 
for this component (no camel-" + scheme
+                                 + "-starter)";
+            default -> scheme + ": the " + name + " runtime has no support for 
this component";
+        };
+    }
+
     /** Options models write that the component does not have, and what the 
component does instead. */
     static final Map<String, String> INVENTED_OPTIONS = Map.ofEntries(
             Map.entry("file:mkdir", "directories are created by default 
(autoCreate=true); remove the option"),
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
index 463e416d2284..939c3672a0a1 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
@@ -26,12 +26,15 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import com.networknt.schema.Error;
 import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.common.CatalogLoader;
 import org.apache.camel.dsl.yaml.validator.YamlValidator;
 
 import static 
org.apache.camel.dsl.jbang.core.commands.ai.JavaChecks.JAVA_CLASS_PATTERN;
@@ -47,7 +50,10 @@ import static 
org.apache.camel.dsl.jbang.core.commands.ai.JavaChecks.withSibling
  */
 public final class SourceValidator {
 
+    private static final String BUILTIN_VERSION = new 
DefaultCamelCatalog().getCatalogVersion();
     private static volatile YamlValidator yamlValidator;
+    /** The schema validators of other Camel versions, by version 
(CAMEL-24711). */
+    private static final Map<String, YamlValidator> VERSION_VALIDATORS = new 
ConcurrentHashMap<>();
 
     private SourceValidator() {
     }
@@ -83,9 +89,19 @@ public final class SourceValidator {
     public static List<String> validate(
             String fileName, String content, CamelCatalog catalog, 
Function<String, String> extraPropertyLine,
             Path directory) {
+        return validate(fileName, content, catalog, extraPropertyLine, 
directory, null);
+    }
+
+    /**
+     * As {@link #validate(String, String, CamelCatalog, Function, Path)}, 
with the YAML DSL schema validator to use:
+     * one built for the schema of another Camel version, or null for the 
schema of the catalog's version.
+     */
+    public static List<String> validate(
+            String fileName, String content, CamelCatalog catalog, 
Function<String, String> extraPropertyLine,
+            Path directory, YamlValidator schemaValidator) {
         String name = fileName == null ? "" : 
fileName.toLowerCase(Locale.ROOT);
         if (name.endsWith(".yaml") || name.endsWith(".yml")) {
-            List<String> msgs = validateCamelYaml(content, catalog);
+            List<String> msgs = validateCamelYaml(content, catalog, 
schemaValidator);
             if (directory != null && msgs.isEmpty()) {
                 msgs = new ArrayList<>(msgs);
                 msgs.addAll(validateYamlBeanRefs(content, 
BeanDeclarations.scan(directory, fileName), catalog));
@@ -114,15 +130,33 @@ public final class SourceValidator {
 
     /**
      * Validates Camel YAML DSL source: the YAML DSL schema first, then 
endpoint URIs and simple expressions against the
-     * catalog. Returns the messages, empty when the source is valid.
+     * catalog. The schema is the one of the catalog's Camel version: the 
CLI's own, or for a catalog of another version
+     * the schema read from the {@code camel-yaml-dsl} jar of that version. 
Returns the messages, empty when the source
+     * is valid.
      */
     public static List<String> validateCamelYaml(String content, CamelCatalog 
catalog) {
+        return validateCamelYaml(content, catalog, null);
+    }
+
+    /**
+     * As {@link #validateCamelYaml(String, CamelCatalog)} with the schema 
validator to use, null for the one of the
+     * catalog's version.
+     */
+    public static List<String> validateCamelYaml(String content, CamelCatalog 
catalog, YamlValidator schemaValidator) {
         List<String> msgs = new ArrayList<>();
         if (content == null || content.isBlank()) {
             return msgs;
         }
+        YamlValidator validator;
+        try {
+            validator = schemaValidator != null ? schemaValidator : 
yamlValidator(catalog);
+        } catch (Exception e) {
+            msgs.add("Cannot validate against the YAML DSL schema of Camel " + 
catalog.getCatalogVersion() + ": "
+                     + e.getMessage());
+            return msgs;
+        }
         try {
-            msgs.addAll(formatSchemaErrors(yamlValidator().validate(content)));
+            msgs.addAll(formatSchemaErrors(validator.validate(content)));
         } catch (Exception e) {
             msgs.add("Invalid YAML: " + e.getMessage());
             return msgs;
@@ -131,6 +165,33 @@ public final class SourceValidator {
         return msgs;
     }
 
+    /**
+     * The schema validator of the catalog's Camel version: the CLI's own when 
the catalog is the built-in one, else one
+     * for the schema of the version the catalog was loaded for (CAMEL-24711), 
built once per version.
+     */
+    static YamlValidator yamlValidator(CamelCatalog catalog) throws Exception {
+        String version = catalog != null ? catalog.getCatalogVersion() : null;
+        if (version == null || version.equals(BUILTIN_VERSION)) {
+            return yamlValidator();
+        }
+        YamlValidator v = VERSION_VALIDATORS.get(version);
+        if (v == null) {
+            synchronized (VERSION_VALIDATORS) {
+                v = VERSION_VALIDATORS.get(version);
+                if (v == null) {
+                    String schema = CatalogLoader.loadYamlDslSchema(null, 
version, false, true);
+                    if (schema == null) {
+                        return yamlValidator();
+                    }
+                    v = new YamlValidator(false, schema, catalog);
+                    v.init();
+                    VERSION_VALIDATORS.put(version, v);
+                }
+            }
+        }
+        return v;
+    }
+
     /**
      * The catalog checks of a YAML route without the schema: endpoint URIs 
and Simple expressions. For a caller that
      * has already validated the schema (the CLI, the schema tool).
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CatalogLoader.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CatalogLoader.java
index e581eb20479d..b17c61d887a7 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CatalogLoader.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CatalogLoader.java
@@ -19,8 +19,11 @@ package org.apache.camel.dsl.jbang.core.common;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.List;
 import java.util.function.Function;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
 
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
@@ -240,6 +243,51 @@ public final class CatalogLoader {
         return answer;
     }
 
+    /**
+     * The YAML DSL JSON schema of a Camel version, read from the {@code 
org.apache.camel:camel-yaml-dsl} jar of that
+     * version (every 4.x release ships {@code schema/camelYamlDsl.json}; the 
canonical schema exists from 4.22).
+     *
+     * @param  repos       extra Maven repositories, comma separated; null for 
the defaults
+     * @param  version     the Camel version
+     * @param  canonical   whether to read the canonical schema
+     * @param  download    whether to download when the jar is not in the 
local repository
+     * @return             the schema document as JSON, or null when the 
version is the one of the CLI, whose schema is
+     *                     on the classpath
+     * @throws IOException when the jar cannot be downloaded or has no such 
schema
+     */
+    public static String loadYamlDslSchema(String repos, String version, 
boolean canonical, boolean download)
+            throws Exception {
+        if (version == null || version.isBlank() || version.equals(new 
DefaultCamelCatalog().getCatalogVersion())) {
+            return null;
+        }
+        String entry = canonical ? "schema/camelYamlDsl-canonical.json" : 
"schema/camelYamlDsl.json";
+        MavenDependencyDownloader downloader = new MavenDependencyDownloader();
+        downloader.setRepositories(repos);
+        downloader.setDownload(download);
+        try {
+            downloader.start();
+            MavenArtifact ma = downloader.downloadArtifact("org.apache.camel", 
"camel-yaml-dsl", version);
+            if (ma == null || ma.getFile() == null) {
+                throw new IOException("Cannot download 
org.apache.camel:camel-yaml-dsl:" + version);
+            }
+            try (ZipFile jar = new ZipFile(ma.getFile())) {
+                ZipEntry ze = jar.getEntry(entry);
+                if (ze == null) {
+                    throw new IOException(
+                            canonical
+                                    ? "Camel " + version + " has no canonical 
YAML DSL schema (it exists from Camel 4.22)"
+                                    : "org.apache.camel:camel-yaml-dsl:" + 
version + " has no " + entry);
+                }
+                try (InputStream is = jar.getInputStream(ze)) {
+                    return new String(is.readAllBytes(), 
StandardCharsets.UTF_8);
+                }
+            }
+        } finally {
+            downloader.stop();
+            downloader.close();
+        }
+    }
+
     public static String resolveCamelVersionFromSpringBoot(String repos, 
String camelSpringBootVersion, boolean download)
             throws Exception {
         DependencyDownloaderClassLoader cl = new 
DependencyDownloaderClassLoader(CatalogLoader.class.getClassLoader());
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorVersionTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorVersionTest.java
new file mode 100644
index 000000000000..b105e6e0c8e4
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorVersionTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.jbang.core.commands.ai;
+
+import java.util.List;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.catalog.DefaultRuntimeProvider;
+import org.apache.camel.dsl.jbang.core.common.CatalogLoader;
+import org.apache.camel.dsl.yaml.validator.YamlValidator;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The validation of a source for another Camel version or runtime than the 
CLI's own (CAMEL-24711): the schema of that
+ * version, and a component the runtime has no extension for.
+ */
+class SourceValidatorVersionTest {
+
+    private static final String ROUTE = """
+            - from:
+                uri: timer:tick
+                steps:
+                  - to: kafka:orders
+                  - to: log:done
+            """;
+
+    /** A catalog that knows timer and log only, the way the 
camel-quarkus-catalog knows the extensions. */
+    static final class FakeQuarkusProvider extends DefaultRuntimeProvider {
+        @Override
+        public String getProviderName() {
+            return "quarkus";
+        }
+
+        @Override
+        public String getComponentJSonSchemaDirectory() {
+            return 
"org/apache/camel/dsl/jbang/core/commands/ai/fakequarkus/components";
+        }
+    }
+
+    @Test
+    void aComponentWithoutAnExtensionIsAnErrorOnTheRuntimeCatalog() {
+        CamelCatalog quarkus = new DefaultCamelCatalog();
+        quarkus.setRuntimeProvider(new FakeQuarkusProvider());
+
+        List<String> msgs = SourceValidator.validateYamlEndpoints(ROUTE, 
quarkus);
+        assertThat(msgs).hasSize(1);
+        assertThat(msgs.get(0)).contains("kafka: Camel Quarkus has no 
extension").contains("camel-quarkus-kafka");
+
+        // the plain catalog says nothing about a scheme it does not know: a 
project can register a component of its own
+        
assertThat(SourceValidator.validateYamlEndpoints(ROUTE.replace("kafka:orders", 
"mine:orders"),
+                new DefaultCamelCatalog())).isEmpty();
+        // and neither does the runtime catalog, only Camel components without 
an extension are reported
+        
assertThat(SourceValidator.validateYamlEndpoints(ROUTE.replace("kafka:orders", 
"mine:orders"), quarkus)).isEmpty();
+    }
+
+    @Test
+    void theBuiltInCatalogUsesTheBuiltInSchema() throws Exception {
+        YamlValidator v = SourceValidator.yamlValidator(new 
DefaultCamelCatalog());
+        assertThat(v).isSameAs(SourceValidator.yamlValidator(null));
+    }
+
+    @Test
+    @DisabledIfSystemProperty(named = "ci.env.name", matches = ".*",
+                              disabledReason = "Runs only local — requires the 
camel-yaml-dsl jar of another version")
+    void aCatalogOfAnotherVersionGetsTheSchemaOfThatVersion() throws Exception 
{
+        CamelCatalog older = CatalogLoader.loadCatalog(null, "4.18.0", true);
+        assertThat(older.getCatalogVersion()).isEqualTo("4.18.0");
+
+        YamlValidator v = SourceValidator.yamlValidator(older);
+        assertThat(v).isNotSameAs(SourceValidator.yamlValidator(null));
+        assertThat(SourceValidator.yamlValidator(older)).as("one validator per 
version").isSameAs(v);
+
+        // a route both versions accept validates; the schema read from the 
4.18.0 jar is the one that answers
+        assertThat(SourceValidator.validateCamelYaml(ROUTE, older)).isEmpty();
+        assertThat(CatalogLoader.loadYamlDslSchema(null, "4.18.0", false, 
true)).contains("\"$schema\"");
+        // the CLI's own version has its schema on the classpath: nothing to 
read
+        assertThat(CatalogLoader.loadYamlDslSchema(null, new 
DefaultCamelCatalog().getCatalogVersion(), false, true))
+                .isNull();
+    }
+
+    @Test
+    @DisabledIfSystemProperty(named = "ci.env.name", matches = ".*",
+                              disabledReason = "Runs only local — requires the 
camel-yaml-dsl jar of another version")
+    void theCanonicalSchemaExistsFromCamel422() {
+        org.assertj.core.api.Assertions.assertThatThrownBy(() -> 
CatalogLoader.loadYamlDslSchema(null, "4.18.0", true, true))
+                .hasMessageContaining("no canonical YAML DSL schema")
+                .hasMessageContaining("4.22");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/org/apache/camel/dsl/jbang/core/commands/ai/fakequarkus/components/log.json
 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/org/apache/camel/dsl/jbang/core/commands/ai/fakequarkus/components/log.json
new file mode 100644
index 000000000000..553eac237c7d
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/org/apache/camel/dsl/jbang/core/commands/ai/fakequarkus/components/log.json
@@ -0,0 +1,68 @@
+{
+  "component": {
+    "kind": "component",
+    "name": "log",
+    "title": "Log Data",
+    "description": "Prints data from the routed message (such as body and 
headers) to the logger.",
+    "deprecated": false,
+    "firstVersion": "1.1.0",
+    "label": "core,monitoring",
+    "javaType": "org.apache.camel.component.log.LogComponent",
+    "supportLevel": "Stable",
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-log",
+    "version": "4.23.0-SNAPSHOT",
+    "scheme": "log",
+    "extendsScheme": "",
+    "syntax": "log:loggerName",
+    "async": false,
+    "api": false,
+    "consumerOnly": false,
+    "producerOnly": true,
+    "lenientProperties": false,
+    "browsable": false,
+    "remote": false
+  },
+  "componentProperties": {
+    "lazyStartProducer": { "index": 0, "kind": "property", "displayName": 
"Lazy Start Producer", "group": "producer", "label": "producer", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": false, "description": 
"Whether the producer should be started lazy (on the first message). By 
starting lazy you can use this to allow CamelContext and routes to startup in 
situations where a producer may otherwise fail [...]
+    "sourceLocationLoggerName": { "index": 1, "kind": "property", 
"displayName": "Source Location Logger Name", "group": "producer", "label": "", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "If enabled then the source location of where the log endpoint 
is used in Camel routes, would be used as logger name, instead of the given 
name. However, if the source location is disabled  [...]
+    "autowiredEnabled": { "index": 2, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching t [...]
+    "exchangeFormatter": { "index": 3, "kind": "property", "displayName": 
"Exchange Formatter", "group": "advanced", "label": "advanced", "required": 
false, "type": "object", "javaType": "org.apache.camel.spi.ExchangeFormatter", 
"deprecated": false, "autowired": true, "secret": false, "description": "Sets a 
custom ExchangeFormatter to convert the Exchange to a String suitable for 
logging. If not specified, we default to DefaultExchangeFormatter." }
+  },
+  "properties": {
+    "loggerName": { "index": 0, "kind": "path", "displayName": "Logger Name", 
"group": "producer", "label": "", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Name of the logging 
category to use" },
+    "groupActiveOnly": { "index": 1, "kind": "parameter", "displayName": 
"Group Active Only", "group": "producer", "label": "", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": true, "description": "If true, will 
hide stats when no new messages have been received for a time interval, if 
false, show stats regardless of message traffic." },
+    "groupDelay": { "index": 2, "kind": "parameter", "displayName": "Group 
Delay", "group": "producer", "label": "", "required": false, "type": "integer", 
"javaType": "java.lang.Long", "deprecated": false, "autowired": false, 
"secret": false, "description": "Set the initial delay for stats (in millis)" },
+    "groupInterval": { "index": 3, "kind": "parameter", "displayName": "Group 
Interval", "group": "producer", "label": "", "required": false, "type": 
"integer", "javaType": "java.lang.Long", "deprecated": false, "autowired": 
false, "secret": false, "description": "If specified will group message stats 
by this time interval (in millis)" },
+    "groupSize": { "index": 4, "kind": "parameter", "displayName": "Group 
Size", "group": "producer", "label": "", "required": false, "type": "integer", 
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false, 
"secret": false, "description": "An integer that specifies a group size for 
throughput logging." },
+    "level": { "index": 5, "kind": "parameter", "displayName": "Level", 
"group": "producer", "label": "", "required": false, "type": "enum", 
"javaType": "java.lang.String", "enum": [ "TRACE", "DEBUG", "INFO", "WARN", 
"ERROR", "OFF" ], "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": "INFO", "description": "Logging level to use. The default value 
is INFO." },
+    "logMask": { "index": 6, "kind": "parameter", "displayName": "Log Mask", 
"group": "producer", "label": "", "required": false, "type": "boolean", 
"javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": false, "description": "If true, mask sensitive 
information like password or passphrase in the log." },
+    "marker": { "index": 7, "kind": "parameter", "displayName": "Marker", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "An optional Marker name to use." },
+    "plain": { "index": 8, "kind": "parameter", "displayName": "Plain", 
"group": "producer", "label": "", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "defaultValue": false, "description": "If enabled only the body will be 
printed out" },
+    "sourceLocationLoggerName": { "index": 9, "kind": "parameter", 
"displayName": "Source Location Logger Name", "group": "producer", "label": "", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "If enabled then the source location of where the log endpoint 
is used in Camel routes, would be used as logger name, instead of the given 
name. However, if the source location is disabled [...]
+    "lazyStartProducer": { "index": 10, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produ [...]
+    "exchangeFormatter": { "index": 11, "kind": "parameter", "displayName": 
"Exchange Formatter", "group": "advanced", "label": "advanced", "required": 
false, "type": "object", "javaType": "org.apache.camel.spi.ExchangeFormatter", 
"deprecated": false, "autowired": false, "secret": false, "description": "To 
use a custom exchange formatter" },
+    "maxChars": { "index": 12, "kind": "parameter", "displayName": "Max 
Chars", "group": "formatting", "label": "formatting", "required": false, 
"type": "integer", "javaType": "int", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 10000, "description": "Limits the number of 
characters logged per line." },
+    "multiline": { "index": 13, "kind": "parameter", "displayName": 
"Multiline", "group": "formatting", "label": "formatting", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "If enabled then 
each information is outputted on a newline." },
+    "showAll": { "index": 14, "kind": "parameter", "displayName": "Show All", 
"group": "formatting", "label": "formatting", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": false, "description": "Quick option for 
turning all options on. (multiline, maxChars has to be manually set if to be 
used)" },
+    "showAllProperties": { "index": 15, "kind": "parameter", "displayName": 
"Show All Properties", "group": "formatting", "label": "formatting", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Show all of the exchange properties (both internal and 
custom)." },
+    "showBody": { "index": 16, "kind": "parameter", "displayName": "Show 
Body", "group": "formatting", "label": "formatting", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": true, "description": "Show the message body." 
},
+    "showBodyType": { "index": 17, "kind": "parameter", "displayName": "Show 
Body Type", "group": "formatting", "label": "formatting", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": true, "description": "Show the body 
Java type." },
+    "showCachedStreams": { "index": 18, "kind": "parameter", "displayName": 
"Show Cached Streams", "group": "formatting", "label": "formatting", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": true, 
"description": "Whether Camel should show cached stream bodies or not 
(org.apache.camel.StreamCache)." },
+    "showCaughtException": { "index": 19, "kind": "parameter", "displayName": 
"Show Caught Exception", "group": "formatting", "label": "formatting", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "If the exchange has a caught exception, show the exception 
message (no stack trace). A caught exception is stored as a property on the 
exchange (using the key org.apache.camel.Exchange# [...]
+    "showException": { "index": 20, "kind": "parameter", "displayName": "Show 
Exception", "group": "formatting", "label": "formatting", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "If the exchange 
has an exception, show the exception message (no stacktrace)" },
+    "showExchangeId": { "index": 21, "kind": "parameter", "displayName": "Show 
Exchange Id", "group": "formatting", "label": "formatting", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "Show the unique 
exchange ID." },
+    "showExchangePattern": { "index": 22, "kind": "parameter", "displayName": 
"Show Exchange Pattern", "group": "formatting", "label": "formatting", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": false, 
"description": "Shows the Message Exchange Pattern (or MEP for short)." },
+    "showFiles": { "index": 23, "kind": "parameter", "displayName": "Show 
Files", "group": "formatting", "label": "formatting", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "If enabled Camel 
will output files" },
+    "showFuture": { "index": 24, "kind": "parameter", "displayName": "Show 
Future", "group": "formatting", "label": "formatting", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "If enabled Camel 
will on Future objects wait for it to complete to obtain the payload to be 
logged." },
+    "showHeaders": { "index": 25, "kind": "parameter", "displayName": "Show 
Headers", "group": "formatting", "label": "formatting", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "Show the message 
headers." },
+    "showProperties": { "index": 26, "kind": "parameter", "displayName": "Show 
Properties", "group": "formatting", "label": "formatting", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "Show the 
exchange properties (only custom). Use showAllProperties to show both internal 
and custom properties." },
+    "showRouteGroup": { "index": 27, "kind": "parameter", "displayName": "Show 
Route Group", "group": "formatting", "label": "formatting", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "Show route 
Group." },
+    "showRouteId": { "index": 28, "kind": "parameter", "displayName": "Show 
Route Id", "group": "formatting", "label": "formatting", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "Show route ID." 
},
+    "showStackTrace": { "index": 29, "kind": "parameter", "displayName": "Show 
Stack Trace", "group": "formatting", "label": "formatting", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "Show the stack 
trace, if an exchange has an exception. Only effective if one of showAll, 
showException or showCaughtException are enabled." },
+    "showStreams": { "index": 30, "kind": "parameter", "displayName": "Show 
Streams", "group": "formatting", "label": "formatting", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "Whether Camel 
should show stream bodies or not (eg such as java.io.InputStream). Beware if 
you enable this option then you may not be able later to access the message 
body as the stream have already bee [...]
+    "showVariables": { "index": 31, "kind": "parameter", "displayName": "Show 
Variables", "group": "formatting", "label": "formatting", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "Show the 
variables." },
+    "skipBodyLineSeparator": { "index": 32, "kind": "parameter", 
"displayName": "Skip Body Line Separator", "group": "formatting", "label": 
"formatting", "required": false, "type": "boolean", "javaType": "boolean", 
"deprecated": false, "autowired": false, "secret": false, "defaultValue": true, 
"description": "Whether to skip line separators when logging the message body. 
This allows to log the message body in one line, setting this option to false 
will preserve any line separators from t [...]
+    "style": { "index": 33, "kind": "parameter", "displayName": "Style", 
"group": "formatting", "label": "formatting", "required": false, "type": 
"enum", "javaType": 
"org.apache.camel.support.processor.DefaultExchangeFormatter.OutputStyle", 
"enum": [ "Default", "Tab", "Fixed" ], "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": "Default", "description": "Sets the outputs 
style to use." }
+  }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/org/apache/camel/dsl/jbang/core/commands/ai/fakequarkus/components/timer.json
 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/org/apache/camel/dsl/jbang/core/commands/ai/fakequarkus/components/timer.json
new file mode 100644
index 000000000000..30d99544faab
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/org/apache/camel/dsl/jbang/core/commands/ai/fakequarkus/components/timer.json
@@ -0,0 +1,52 @@
+{
+  "component": {
+    "kind": "component",
+    "name": "timer",
+    "title": "Timer",
+    "description": "Generate messages in specified intervals using 
java.util.Timer.",
+    "deprecated": false,
+    "firstVersion": "1.0.0",
+    "label": "core,scheduling",
+    "javaType": "org.apache.camel.component.timer.TimerComponent",
+    "supportLevel": "Stable",
+    "groupId": "org.apache.camel",
+    "artifactId": "camel-timer",
+    "version": "4.23.0-SNAPSHOT",
+    "scheme": "timer",
+    "extendsScheme": "",
+    "syntax": "timer:timerName",
+    "async": false,
+    "api": false,
+    "consumerOnly": true,
+    "producerOnly": false,
+    "lenientProperties": false,
+    "browsable": false,
+    "remote": false
+  },
+  "componentProperties": {
+    "bridgeErrorHandler": { "index": 0, "kind": "property", "displayName": 
"Bridge Error Handler", "group": "consumer", "label": "consumer", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": false, "description": 
"Allows for bridging the consumer to the Camel routing Error Handler, which 
mean any exceptions (if possible) occurred while the Camel consumer is trying 
to pickup incoming messages, or the like [...]
+    "includeMetadata": { "index": 1, "kind": "property", "displayName": 
"Include Metadata", "group": "consumer", "label": "", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "Whether to 
include metadata in the exchange such as fired time, timer name, timer count 
etc." },
+    "autowiredEnabled": { "index": 2, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching t [...]
+  },
+  "headers": {
+    "CamelTimerFiredTime": { "index": 0, "kind": "header", "displayName": "", 
"group": "consumer", "label": "", "required": false, "javaType": "Date", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The fired time", "constantName": 
"org.apache.camel.component.timer.TimerConstants#HEADER_FIRED_TIME" },
+    "CamelMessageTimestamp": { "index": 1, "kind": "header", "displayName": 
"", "group": "consumer", "label": "", "required": false, "javaType": "long", 
"deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The timestamp of the message", "constantName": 
"org.apache.camel.component.timer.TimerConstants#HEADER_MESSAGE_TIMESTAMP" }
+  },
+  "properties": {
+    "timerName": { "index": 0, "kind": "path", "displayName": "Timer Name", 
"group": "consumer", "label": "", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "The name of the timer" },
+    "delay": { "index": 1, "kind": "parameter", "displayName": "Delay", 
"group": "consumer", "label": "", "required": false, "type": "duration", 
"javaType": "long", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": "1000", "description": "The number of milliseconds to wait 
before the first event is generated. Should not be used in conjunction with the 
time option. The default value is 1000." },
+    "fixedRate": { "index": 2, "kind": "parameter", "displayName": "Fixed 
Rate", "group": "consumer", "label": "", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "defaultValue": false, "description": "Events take place at 
approximately regular intervals, separated by the specified period." },
+    "includeMetadata": { "index": 3, "kind": "parameter", "displayName": 
"Include Metadata", "group": "consumer", "label": "", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "Whether to 
include metadata in the exchange such as fired time, timer name, timer count 
etc." },
+    "period": { "index": 4, "kind": "parameter", "displayName": "Period", 
"group": "consumer", "label": "", "required": false, "type": "duration", 
"javaType": "long", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": "1000", "description": "Generate periodic events every period. 
Must be zero or positive value. The default value is 1000." },
+    "repeatCount": { "index": 5, "kind": "parameter", "displayName": "Repeat 
Count", "group": "consumer", "label": "", "required": false, "type": "integer", 
"javaType": "long", "deprecated": false, "autowired": false, "secret": false, 
"description": "Specifies a maximum limit for the number of fires. Therefore, 
if you set it to 1, the timer will only fire once. If you set it to 5, it will 
only fire five times. A value of zero or negative means fire forever." },
+    "bridgeErrorHandler": { "index": 6, "kind": "parameter", "displayName": 
"Bridge Error Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Allows for bridging the consumer to the 
Camel routing Error Handler, which mean any exceptions (if possible) occurred 
while the Camel consumer is trying to pickup incoming  [...]
+    "exceptionHandler": { "index": 7, "kind": "parameter", "displayName": 
"Exception Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "object", "javaType": 
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", 
"deprecated": false, "autowired": false, "secret": false, "description": "To 
let the consumer use a custom ExceptionHandler. Notice if the option 
bridgeErrorHandler is enabled then this option is not in use. By def [...]
+    "exchangePattern": { "index": 8, "kind": "parameter", "displayName": 
"Exchange Pattern", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "enum", "javaType": 
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], 
"deprecated": false, "autowired": false, "secret": false, "description": "Sets 
the exchange pattern when the consumer creates an exchange." },
+    "daemon": { "index": 9, "kind": "parameter", "displayName": "Daemon", 
"group": "advanced", "label": "advanced", "required": false, "type": "boolean", 
"javaType": "boolean", "deprecated": false, "autowired": false, "secret": 
false, "defaultValue": true, "description": "Specifies whether the thread 
associated with the timer endpoint runs as a daemon. The default value is 
true." },
+    "pattern": { "index": 10, "kind": "parameter", "displayName": "Pattern", 
"group": "advanced", "label": "advanced", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "Allows you to specify a custom Date pattern to 
use for setting the time option using URI syntax." },
+    "synchronous": { "index": 11, "kind": "parameter", "displayName": 
"Synchronous", "group": "advanced", "label": "advanced", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "description": "Sets whether 
synchronous processing should be strictly used" },
+    "time": { "index": 12, "kind": "parameter", "displayName": "Time", 
"group": "advanced", "label": "advanced", "required": false, "type": "string", 
"javaType": "java.util.Date", "deprecated": false, "autowired": false, 
"secret": false, "description": "A java.util.Date the first event should be 
generated. If using the URI, the pattern expected is: yyyy-MM-dd HH:mm:ss or 
yyyy-MM-dd'T'HH:mm:ss." },
+    "timer": { "index": 13, "kind": "parameter", "displayName": "Timer", 
"group": "advanced", "label": "advanced", "required": false, "type": "object", 
"javaType": "java.util.Timer", "deprecated": false, "autowired": false, 
"secret": false, "description": "To use a custom Timer" },
+    "runLoggingLevel": { "index": 14, "kind": "parameter", "displayName": "Run 
Logging Level", "group": "scheduler", "label": "consumer,scheduler", 
"required": false, "type": "enum", "javaType": "org.apache.camel.LoggingLevel", 
"enum": [ "TRACE", "DEBUG", "INFO", "WARN", "ERROR", "OFF" ], "deprecated": 
false, "autowired": false, "secret": false, "defaultValue": "TRACE", 
"description": "The consumer logs a start\/complete log line when it polls. 
This option allows you to configure the log [...]
+  }
+}
diff --git a/dsl/camel-jbang/camel-jbang-plugin-validate/pom.xml 
b/dsl/camel-jbang/camel-jbang-plugin-validate/pom.xml
index 8a281548b22a..d108e6dfdf2b 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-validate/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-plugin-validate/pom.xml
@@ -49,5 +49,17 @@
             <groupId>org.apache.camel</groupId>
             <artifactId>camel-yaml-dsl-validator</artifactId>
         </dependency>
+
+        <!-- test -->
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-test-junit6</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/CatalogVersionMixin.java
 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/CatalogVersionMixin.java
new file mode 100644
index 000000000000..4336a9c46092
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/CatalogVersionMixin.java
@@ -0,0 +1,98 @@
+/*
+ * 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.jbang.core.commands.validate;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.MavenResolverMixin;
+import org.apache.camel.dsl.jbang.core.commands.QuarkusPlatformMixin;
+import org.apache.camel.dsl.jbang.core.common.CatalogLoader;
+import org.apache.camel.dsl.jbang.core.common.QuarkusHelper;
+import org.apache.camel.dsl.jbang.core.common.RuntimeCompletionCandidates;
+import org.apache.camel.dsl.jbang.core.common.RuntimeType;
+import org.apache.camel.dsl.jbang.core.common.RuntimeTypeConverter;
+import org.apache.camel.dsl.yaml.validator.YamlValidator;
+import picocli.CommandLine;
+
+/**
+ * The Camel version and runtime a validation answers for (CAMEL-24711): the 
catalog of that version and runtime, as
+ * {@code camel catalog} and {@code camel doc} load it, and the YAML DSL 
schema of that Camel version read from its
+ * {@code camel-yaml-dsl} jar. Without options the CLI's own version, with 
nothing to download.
+ */
+public class CatalogVersionMixin {
+
+    @CommandLine.Option(names = { "--camel-version" },
+                        description = "Validate against another Camel version 
than the CLI's own: its catalog and YAML DSL schema")
+    String camelVersion;
+
+    @CommandLine.Option(names = { "--runtime" },
+                        completionCandidates = 
RuntimeCompletionCandidates.class,
+                        converter = RuntimeTypeConverter.class,
+                        description = "Runtime (${COMPLETION-CANDIDATES}); 
spring-boot and quarkus use the catalog of that"
+                                      + " runtime, so a component without a 
starter or extension is an error")
+    RuntimeType runtime;
+
+    @CommandLine.Mixin
+    MavenResolverMixin mavenResolver;
+
+    @CommandLine.Mixin
+    QuarkusPlatformMixin quarkusPlatform;
+
+    /** The catalog and the Camel version it answers for. */
+    public record Loaded(CamelCatalog catalog, String camelVersion) {
+    }
+
+    /**
+     * Loads the catalog of the version and runtime asked for; the built-in 
catalog when none is.
+     */
+    public Loaded load() throws Exception {
+        if (RuntimeType.springBoot == runtime) {
+            String version = camelVersion != null ? camelVersion : new 
DefaultCamelCatalog().getCatalogVersion();
+            return new Loaded(
+                    CatalogLoader.loadSpringBootCatalog(mavenResolver.repos(), 
version, mavenResolver.download()), version);
+        }
+        if (RuntimeType.quarkus == runtime) {
+            QuarkusHelper.QuarkusPlatformBom bom = 
quarkusPlatform.resolve(camelVersion,
+                    mavenResolver.downloader()::resolveArtifact, 
mavenResolver.download(), mavenResolver.fresh());
+            CamelCatalog catalog
+                    = CatalogLoader.loadQuarkusCatalog(bom.quarkusCamelBom(), 
mavenResolver.downloader()::resolveArtifact);
+            // the Camel version the platform pins is the one the schema is 
read for
+            String version = bom.camelVersion() != null ? bom.camelVersion() : 
camelVersion;
+            return new Loaded(catalog, version);
+        }
+        if (camelVersion == null || camelVersion.isBlank()) {
+            return new Loaded(new DefaultCamelCatalog(), null);
+        }
+        return new Loaded(
+                CatalogLoader.loadCatalog(mavenResolver.repos(), camelVersion, 
mavenResolver.download()),
+                camelVersion);
+    }
+
+    /**
+     * The YAML DSL schema validator of the loaded Camel version: the CLI's 
own schema for the CLI's version, else the
+     * schema read from the {@code camel-yaml-dsl} jar of that version.
+     *
+     * @throws Exception when the jar cannot be downloaded, or the version has 
no canonical schema (before 4.22)
+     */
+    public YamlValidator yamlValidator(Loaded loaded, boolean canonical) 
throws Exception {
+        String schema = CatalogLoader.loadYamlDslSchema(mavenResolver.repos(), 
loaded.camelVersion(), canonical,
+                mavenResolver.download());
+        YamlValidator validator = new YamlValidator(canonical, schema, 
loaded.catalog());
+        validator.init();
+        return validator;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/SourceValidateCommand.java
 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/SourceValidateCommand.java
index 71ec89464f83..b5fa97808a53 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/SourceValidateCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/SourceValidateCommand.java
@@ -25,10 +25,10 @@ import java.util.Map;
 import java.util.Stack;
 
 import org.apache.camel.catalog.CamelCatalog;
-import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator;
+import org.apache.camel.dsl.yaml.validator.YamlValidator;
 import picocli.CommandLine;
 
 /**
@@ -45,13 +45,19 @@ public class SourceValidateCommand extends CamelCommand {
                             parameterConsumer = FilesConsumer.class)
     List<String> files = new ArrayList<>();
 
+    @CommandLine.Mixin
+    CatalogVersionMixin catalogVersion;
+
     public SourceValidateCommand(CamelJBangMain main) {
         super(main);
     }
 
     @Override
     public Integer doCall() throws Exception {
-        CamelCatalog catalog = new DefaultCamelCatalog();
+        // the catalog and schema of the Camel version and runtime asked for; 
the CLI's own without options
+        CatalogVersionMixin.Loaded loaded = catalogVersion.load();
+        CamelCatalog catalog = loaded.catalog();
+        YamlValidator schemaValidator = loaded.camelVersion() != null ? 
catalogVersion.yamlValidator(loaded, false) : null;
         Map<String, List<String>> reports = new LinkedHashMap<>();
         for (String n : files) {
             File f = new File(n);
@@ -62,7 +68,7 @@ public class SourceValidateCommand extends CamelCommand {
             String content = Files.readString(f.toPath());
             File parent = f.getAbsoluteFile().getParentFile();
             reports.put(n, SourceValidator.validate(f.getName(), content, 
catalog, null,
-                    parent != null ? parent.toPath() : null));
+                    parent != null ? parent.toPath() : null, schemaValidator));
         }
         int count = reports.values().stream().mapToInt(List::size).sum();
         if (count > 0) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java
 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java
index e0591e317f7a..68d8112311c9 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlValidateCommand.java
@@ -28,7 +28,6 @@ import java.util.Stack;
 
 import com.networknt.schema.Error;
 import org.apache.camel.catalog.CamelCatalog;
-import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator;
@@ -51,6 +50,9 @@ public class YamlValidateCommand extends CamelCommand {
                                       + " (use --catalog=false for the schema 
only)")
     boolean catalog = true;
 
+    @CommandLine.Mixin
+    CatalogVersionMixin catalogVersion;
+
     @CommandLine.Parameters(description = { "The Camel YAML source files to 
parse." },
                             arity = "1..9",
                             paramLabel = "<files>",
@@ -63,10 +65,11 @@ public class YamlValidateCommand extends CamelCommand {
 
     @Override
     public Integer doCall() throws Exception {
-        YamlValidator validator = new YamlValidator(canonical);
-        validator.init();
+        // the catalog and schema of the Camel version and runtime asked for; 
the CLI's own without options
+        CatalogVersionMixin.Loaded loaded = catalogVersion.load();
+        YamlValidator validator = catalogVersion.yamlValidator(loaded, 
canonical);
 
-        CamelCatalog camelCatalog = catalog ? new DefaultCamelCatalog() : null;
+        CamelCatalog camelCatalog = catalog ? loaded.catalog() : null;
         Map<String, List<Error>> reports = new LinkedHashMap<>();
         for (String n : files) {
             if (matchFile(n)) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/ValidateCamelVersionTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/ValidateCamelVersionTest.java
new file mode 100644
index 000000000000..1d6b60b4ffc7
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/ValidateCamelVersionTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.jbang.core.commands.validate;
+
+import java.nio.file.Path;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.StringPrinter;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * camel validate yaml and camel validate source against the catalog and YAML 
DSL schema of another Camel version or
+ * runtime (CAMEL-24711). Without options the CLI's own version answers, 
offline.
+ */
+class ValidateCamelVersionTest {
+
+    private static final String ROUTE = 
Path.of("src/test/resources/route.yaml").toString();
+    private static final String NO_EXTENSION = 
Path.of("src/test/resources/no-extension.yaml").toString();
+
+    /** The file based Quarkus extension registry of the camel-jbang-core 
tests: no registry call. */
+    private static String quarkusExtRegistry() {
+        return "--quarkus-ext-registry="
+               + 
Path.of("../camel-jbang-core/src/test/resources/registry.quarkus.io").toAbsolutePath().normalize().toUri();
+    }
+
+    private static StringPrinter printer;
+
+    private static int yaml(String... args) throws Exception {
+        printer = new StringPrinter();
+        YamlValidateCommand cmd = new YamlValidateCommand(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(cmd, args);
+        return cmd.doCall();
+    }
+
+    private static int source(String... args) throws Exception {
+        printer = new StringPrinter();
+        SourceValidateCommand cmd = new SourceValidateCommand(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(cmd, args);
+        return cmd.doCall();
+    }
+
+    @Test
+    void theOwnVersionValidatesOffline() throws Exception {
+        assertThat(yaml(ROUTE)).isZero();
+        assertThat(printer.getOutput()).contains("Validation success");
+        assertThat(source(ROUTE)).isZero();
+        assertThat(printer.getOutput()).contains("Validation success");
+
+        // the plain catalog knows every component: a Camel component without 
an extension is not its business
+        assertThat(yaml(NO_EXTENSION)).isZero();
+    }
+
+    @Test
+    @DisabledIfSystemProperty(named = "ci.env.name", matches = ".*",
+                              disabledReason = "Runs only local — downloads 
the catalog and camel-yaml-dsl jar of another version")
+    void anotherCamelVersionValidatesWithItsSchemaAndCatalog() throws 
Exception {
+        assertThat(yaml("--camel-version=4.18.0", ROUTE)).isZero();
+        assertThat(printer.getOutput()).contains("Validation success");
+        assertThat(source("--camel-version=4.18.0", ROUTE)).isZero();
+        assertThat(printer.getOutput()).contains("Validation success");
+
+        // the canonical schema exists from 4.22: an older version is an 
error, not a silent fallback
+        assertThatThrownBy(() -> yaml("--canonical", "--camel-version=4.18.0", 
ROUTE))
+                .hasMessageContaining("no canonical YAML DSL 
schema").hasMessageContaining("4.22");
+    }
+
+    @Test
+    @DisabledIfSystemProperty(named = "ci.env.name", matches = ".*",
+                              disabledReason = "Runs only local — downloads 
the Quarkus platform BOM and catalog")
+    void quarkusReportsAComponentWithoutAnExtension() throws Exception {
+        // Quarkus platform 3.30.1 pins Camel 4.16.0: the catalog of 
camel-quarkus-catalog 3.30.0, the schema of 4.16.0
+        assertThat(yaml("--runtime=quarkus", "--quarkus-version=3.30.1", 
quarkusExtRegistry(), ROUTE)).isZero();
+        assertThat(source("--runtime=quarkus", "--quarkus-version=3.30.1", 
quarkusExtRegistry(), ROUTE)).isZero();
+
+        assertThat(yaml("--runtime=quarkus", "--quarkus-version=3.30.1", 
quarkusExtRegistry(), NO_EXTENSION)).isEqualTo(1);
+        assertThat(printer.getOutput()).contains("atmosphere-websocket: Camel 
Quarkus has no extension");
+        assertThat(source("--runtime=quarkus", "--quarkus-version=3.30.1", 
quarkusExtRegistry(), NO_EXTENSION))
+                .isEqualTo(1);
+        assertThat(printer.getOutput()).contains("atmosphere-websocket: Camel 
Quarkus has no extension");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/no-extension.yaml
 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/no-extension.yaml
new file mode 100644
index 000000000000..df6b67904d5b
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/no-extension.yaml
@@ -0,0 +1,21 @@
+#
+# 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.
+#
+
+- from:
+    uri: timer:tick
+    steps:
+      - to: atmosphere-websocket:/chat
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/route.yaml 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/route.yaml
new file mode 100644
index 000000000000..a31e4a593053
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/route.yaml
@@ -0,0 +1,28 @@
+#
+# 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.
+#
+
+- from:
+    uri: timer:tick
+    parameters:
+      period: 1000
+    steps:
+      - setBody:
+          expression:
+            simple:
+              expression: "Hello ${random(1,10)}"
+      - to: kafka:orders
+      - to: log:done
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 366604c23b5a..9a050d5ad4c0 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
@@ -73,6 +73,8 @@ public class YamlValidator {
 
     private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
     private final boolean canonical;
+    private final String schemaJson;
+    private final CamelCatalog catalog;
     private Schema schema;
     private Map<String, OneOfGroup> oneOfGroups;
 
@@ -84,7 +86,22 @@ public class YamlValidator {
     }
 
     public YamlValidator(boolean canonical) {
+        this(canonical, null, null);
+    }
+
+    /**
+     * A validator for a schema document other than the one on the classpath: 
the schema of another Camel version, read
+     * from the {@code camel-yaml-dsl} jar of that version.
+     *
+     * @param canonical  whether the document is the canonical schema
+     * @param schemaJson the schema document as JSON; null for the schema on 
the classpath
+     * @param catalog    the catalog of the same Camel version, for the checks 
the schema cannot express; null for the
+     *                   catalog on the classpath
+     */
+    public YamlValidator(boolean canonical, String schemaJson, CamelCatalog 
catalog) {
         this.canonical = canonical;
+        this.schemaJson = schemaJson;
+        this.catalog = catalog;
     }
 
     public boolean isCanonical() {
@@ -778,9 +795,9 @@ public class YamlValidator {
      * array-typed options (e.g. "outputs") use "oneOf" to mean "each element 
is one of these types", not "exactly one
      * of these sibling keys must be present".
      */
-    private static Map<String, OneOfGroup> loadOneOfGroups() {
+    private Map<String, OneOfGroup> loadOneOfGroups() {
         Map<String, OneOfGroup> groups = new HashMap<>();
-        CamelCatalog catalog = new DefaultCamelCatalog();
+        CamelCatalog catalog = this.catalog != null ? this.catalog : new 
DefaultCamelCatalog();
         for (String name : catalog.findModelNames()) {
             EipModel model = catalog.eipModel(name);
             if (model == null) {
@@ -1270,7 +1287,8 @@ public class YamlValidator {
 
     public void init() throws Exception {
         String location = canonical ? LOCATION_CANONICAL : LOCATION;
-        var model = 
mapper.readTree(YamlValidator.class.getResourceAsStream(location));
+        var model = schemaJson != null
+                ? mapper.readTree(schemaJson) : 
mapper.readTree(YamlValidator.class.getResourceAsStream(location));
         this.model = model;
         this.topLevelEntries = new LinkedHashSet<>();
         
model.at("/items/properties").fieldNames().forEachRemaining(topLevelEntries::add);
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorSchemaDocumentTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorSchemaDocumentTest.java
new file mode 100644
index 000000000000..e0db8ddf47d9
--- /dev/null
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorSchemaDocumentTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.io.File;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * A validator built for a schema document handed to it, as camel validate 
--camel-version reads the schema of another
+ * Camel version from its camel-yaml-dsl jar (CAMEL-24711), validates against 
that document and not the classpath.
+ */
+public class YamlValidatorSchemaDocumentTest {
+
+    private static String schema(String location) throws Exception {
+        try (var is = YamlValidator.class.getResourceAsStream(location)) {
+            return new String(is.readAllBytes(), StandardCharsets.UTF_8);
+        }
+    }
+
+    @Test
+    public void theGivenDocumentIsTheSchema() throws Exception {
+        File shorthand = new 
File("src/test/resources/canonical-invalid-log-shorthand.yaml");
+
+        // the classpath schema accepts the log shorthand
+        YamlValidator classpath = new YamlValidator(false, null, null);
+        classpath.init();
+        assertThat(classpath.validate(shorthand)).isEmpty();
+
+        // the same validator given the canonical document as its schema 
rejects it: the document is what counts
+        YamlValidator given = new YamlValidator(false, 
schema("/schema/camelYamlDsl-canonical.json"), null);
+        given.init();
+        assertThat(given.validate(shorthand)).isNotEmpty();
+        assertThat(given.isCanonical()).isFalse();
+
+        // and the classic document given explicitly validates as the 
classpath one does
+        YamlValidator same = new YamlValidator(false, 
schema("/schema/camelYamlDsl.json"), null);
+        same.init();
+        assertThat(same.validate(shorthand)).isEmpty();
+        assertThat(same.validate(new 
File("src/test/resources/canonical-valid.yaml"))).isEmpty();
+    }
+}

Reply via email to