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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 601c6c97615f CAMEL-24847: camel validate yaml - a marshal/unmarshal 
key that names the data format as its artifact or library says which key to 
write (#26619)
601c6c97615f is described below

commit 601c6c97615f5534a3d11f3c2f4b0d21c048f92e
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Sep 20 10:50:07 2026 +0200

    CAMEL-24847: camel validate yaml - a marshal/unmarshal key that names the 
data format as its artifact or library says which key to write (#26619)
    
    * CAMEL-24847: camel validate yaml - a marshal/unmarshal key that names the 
data format as its artifact or library says which key to write
    
    unmarshal: {jackson: {}}, marshal: {json-jackson: {}} and unmarshal:
    {jackson-xml: {}} were rejected with "property 'jackson' is not defined in
    the schema" by the validator and "unsupported field: jackson" by the
    runtime, neither naming the data format that was meant. The YAML key is the
    data format's model name (json, jacksonXml, yaml); ten catalog data formats
    have a name of their own because one model serves several libraries or
    types (jackson, gson, jsonb, fastjson -> json with library; avroJackson,
    protobufJackson, snakeYaml, bindyCsv/Fixed/Kvp).
    
    DataFormatKeyHints in camel-yaml-dsl-common holds that table with the
    selecting option, and resolves the spellings people use: jackson, Jackson,
    json-jackson, jackson-json, jackson-avro, bindy_csv. The validator's
    marshal/unmarshal row says "the data format is json, Jackson is its
    library: write json: {library: Jackson}"; a key spelled differently gets
    the key (jackson-xml: jacksonXml, JSON: json); a word of a name gets the
    catalog's suggestions (xml: fhirXml, groovyXml or jacksonXml); a typo the
    closest key (jsn: json); anything else what the key is. This also replaces
    two wrong hints the edit-distance fallback gave (gson -> json lost the
    library, bindy-csv -> csv changed the data format). In canonical mode the
    "must have exactly one of [50 data formats]" line that followed the hint
    at the same location is dropped. The runtime deserializer gives the same
    hint for marshal/unmarshal from the table and the deserializer's declared
    keys, without a catalog. A validator test checks the table agrees with the
    catalog.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-24847: address review: alias() owns the normalization, the table 
test rejects a stale entry, a marshal row in the runtime test
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../camel/dsl/yaml/common/DataFormatKeyHints.java  | 145 +++++++++++++++++++++
 .../yaml/common/YamlDeserializationContext.java    |  21 +++
 .../camel/dsl/yaml/validator/SchemaHints.java      |   3 +
 .../camel/dsl/yaml/validator/YamlValidator.java    |  47 ++++++-
 .../validator/DataFormatKeyHintsCatalogTest.java   |  89 +++++++++++++
 .../validator/YamlValidatorPropertyHintTest.java   |  79 +++++++++++
 .../org/apache/camel/dsl/yaml/UnmarshalTest.groovy |  30 +++++
 7 files changed, 412 insertions(+), 2 deletions(-)

diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/DataFormatKeyHints.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/DataFormatKeyHints.java
new file mode 100644
index 000000000000..9f46383bb0c7
--- /dev/null
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/DataFormatKeyHints.java
@@ -0,0 +1,145 @@
+/*
+ * 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.common;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * The hint for a {@code marshal}/{@code unmarshal} key that names a data 
format the way its artifact or the catalog
+ * does (jackson, json-jackson, jackson-xml, snake-yaml) instead of by its 
YAML key (json with library Jackson,
+ * jacksonXml, yaml).
+ * <p>
+ * The YAML key is the data format's model name. Ten data formats in the 
catalog have a name of their own that differs
+ * from it, because one model serves several libraries or types: {@link 
#ALIASES} lists them with the option that
+ * selects the library or type. The table mirrors the catalog ({@code name} 
and {@code modelName} of each data format,
+ * and the enum of the {@code library} or {@code type} option of the model); 
the validator's tests check the two agree.
+ * The deserializer cannot read the catalog at runtime, which is why the table 
is here rather than derived.
+ */
+public final class DataFormatKeyHints {
+
+    /** The YAML key of an aliased data format and the option that selects it: 
json with library Jackson. */
+    public record Alias(String key, String option, String value) {
+
+        /** The key with the option, as it is written in YAML: json: {library: 
Jackson}, or yaml: {...}. */
+        public String form() {
+            return option != null ? key + ": {" + option + ": " + value + "}" 
: key + ": {...}";
+        }
+    }
+
+    /**
+     * The catalog data formats whose name is not their YAML key, by name in 
lower case with no separators, so that
+     * jackson, Jackson, snake-yaml and snakeYaml all match.
+     */
+    public static final Map<String, Alias> ALIASES = Map.ofEntries(
+            Map.entry("jackson", new Alias("json", "library", "Jackson")),
+            Map.entry("gson", new Alias("json", "library", "Gson")),
+            Map.entry("jsonb", new Alias("json", "library", "Jsonb")),
+            Map.entry("fastjson", new Alias("json", "library", "Fastjson")),
+            Map.entry("avrojackson", new Alias("avro", "library", "Jackson")),
+            Map.entry("protobufjackson", new Alias("protobuf", "library", 
"Jackson")),
+            Map.entry("snakeyaml", new Alias("yaml", null, null)),
+            Map.entry("bindycsv", new Alias("bindy", "type", "Csv")),
+            Map.entry("bindyfixed", new Alias("bindy", "type", "Fixed")),
+            Map.entry("bindykvp", new Alias("bindy", "type", "KeyValue")));
+
+    /** The models an alias belongs to, which people prefix or suffix the 
library with: json-jackson, jackson-json. */
+    private static final List<String> MODELS = List.of("json", "avro", 
"protobuf", "yaml", "bindy");
+
+    private DataFormatKeyHints() {
+    }
+
+    /**
+     * The hint for a key that is not a data format key, or null when the key 
is neither a spelling of one of the known
+     * keys nor an alias.
+     *
+     * @param  key       the key as written: jackson, json-jackson, 
jackson-xml, JSON
+     * @param  knownKeys the data format keys of marshal/unmarshal: json, 
jacksonXml, yaml...
+     * @return           the hint: did you mean 'jacksonXml'? for a spelling 
of a key, the data format is json, Jackson
+     *                   is its library: write json: {library: Jackson} for an 
alias, or null
+     */
+    public static String hint(String key, Collection<String> knownKeys) {
+        String normalized = normalize(key);
+        // jackson-xml, JSON, base-64: the key itself, spelled differently
+        for (String known : knownKeys) {
+            if (!known.equals(key) && normalized.equals(normalize(known))) {
+                return "did you mean '" + known + "'?";
+            }
+        }
+        Alias alias = alias(key);
+        return alias != null ? hint(alias) : null;
+    }
+
+    /**
+     * The alias a spelling of a data format name refers to, or null: jackson 
and json-jackson give json with library
+     * Jackson, bindy-csv gives bindy with type Csv.
+     */
+    public static Alias alias(String name) {
+        String normalized = normalize(name);
+        Alias alias = ALIASES.get(normalized);
+        if (alias != null) {
+            return alias;
+        }
+        // json-jackson, jackson-json: the model around the library; 
jackson-avro: the library of another model
+        for (String model : MODELS) {
+            String rest = null;
+            if (normalized.startsWith(model) && normalized.length() > 
model.length()) {
+                rest = normalized.substring(model.length());
+            } else if (normalized.endsWith(model) && normalized.length() > 
model.length()) {
+                rest = normalized.substring(0, normalized.length() - 
model.length());
+            }
+            if (rest == null) {
+                continue;
+            }
+            alias = ALIASES.get(model + rest);
+            if (alias == null) {
+                alias = ALIASES.get(rest);
+                if (alias != null && !alias.key().equals(model)) {
+                    alias = null;
+                }
+            }
+            if (alias != null) {
+                return alias;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * The key with the option that selects a data format, as it is written in 
YAML: json: {library: Jackson} for
+     * jackson, jacksonXml for jacksonXml.
+     */
+    public static String form(String name) {
+        Alias alias = alias(name);
+        return alias != null ? alias.form() : name;
+    }
+
+    private static String hint(Alias alias) {
+        if (alias.option() == null) {
+            return "the data format is " + alias.key() + ": write " + 
alias.form();
+        }
+        return "the data format is " + alias.key() + ", " + alias.value() + " 
is its " + alias.option() + ": write "
+               + alias.form();
+    }
+
+    /** Lower case with no separators: json-jackson, json_jackson, jsonJackson 
and JSON-Jackson are one name. */
+    public static String normalize(String name) {
+        return name.replaceAll("[-_ .]", "").toLowerCase(Locale.ROOT);
+    }
+}
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializationContext.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializationContext.java
index 87bb44f521b5..2703f25ecd85 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializationContext.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializationContext.java
@@ -33,6 +33,8 @@ import 
org.apache.camel.dsl.yaml.common.exception.UnknownNodeIdException;
 import org.apache.camel.dsl.yaml.common.exception.UnsupportedFieldException;
 import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
 import org.apache.camel.spi.Resource;
+import org.apache.camel.spi.annotations.YamlProperty;
+import org.apache.camel.spi.annotations.YamlType;
 import org.apache.camel.support.OrderedComparator;
 import org.apache.camel.util.ObjectHelper;
 import org.slf4j.Logger;
@@ -419,6 +421,12 @@ public class YamlDeserializationContext extends 
StandardConstructor implements C
             } else if ("expression".equals(field) || "language".equals(field)) 
{
                 hint = " (an expression is written with the expression: 
wrapper and the language as the key: expression: {simple:"
                        + " {expression: \"...\"}}, expression: {constant: 
{expression: \"...\"}})";
+            } else if (field != null && ("marshal".equals(id) || 
"unmarshal".equals(id))) {
+                // unmarshal: {jackson: {}}: the data format named as its 
artifact or catalog entry, not by its key
+                String dataFormat = DataFormatKeyHints.hint(field, 
propertyNames(constructor));
+                if (dataFormat != null) {
+                    hint = " (" + dataFormat + ")";
+                }
             }
             throw new YamlDeserializationException(
                     node, "Error constructing YAML node id: " + id + ": 
unsupported field: " + field + hint, e);
@@ -426,4 +434,17 @@ public class YamlDeserializationContext extends 
StandardConstructor implements C
             throw new YamlDeserializationException(node, "Error constructing 
YAML node id: " + id, e);
         }
     }
+
+    /** The property names a deserializer declares: the data format keys of 
marshal and unmarshal. */
+    private static List<String> propertyNames(ConstructNode constructor) {
+        YamlType type = constructor.getClass().getAnnotation(YamlType.class);
+        if (type == null) {
+            return List.of();
+        }
+        List<String> names = new ArrayList<>(type.properties().length);
+        for (YamlProperty property : type.properties()) {
+            names.add(property.name());
+        }
+        return names;
+    }
 }
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
index 7e2c096c1cf6..03c2bea61047 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
@@ -332,6 +332,9 @@ final class SchemaHints {
                     m -> "the name is a property: " + m.name() + ": {name: " + 
m.unknown()
                          + (m.name().startsWith("set") ? ", expression: 
{simple: {expression: \"...\"}}}" : "}")
                          + " (" + m.unknown() + " is not the key)"),
+            // unmarshal: {jackson: {}}: the data format named as its artifact 
or catalog entry, not by its key
+            unknownProperty(".*/(marshal|unmarshal)", ANY,
+                    m -> m.validator().dataFormatHint(m.unknown(), m.name(), 
m.schemaLocation())),
             unknownProperty(".*/bean", m -> Set.of("parameters", "args", 
"arguments").contains(m.unknown()),
                     m -> "arguments are written in the method call: bean: 
{ref: myBean, method: \"process(${body},"
                          + " 'x')\"}"),
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 36fbf825d3c3..128dc98b56ef 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
@@ -48,6 +48,7 @@ import com.networknt.schema.path.NodePath;
 import com.networknt.schema.path.PathType;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.yaml.common.DataFormatKeyHints;
 import org.apache.camel.tooling.model.EipModel;
 
 /**
@@ -76,7 +77,7 @@ public class YamlValidator {
     private final ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
     private final boolean canonical;
     private final String schemaJson;
-    private final CamelCatalog catalog;
+    private CamelCatalog catalog;
     private Schema schema;
     private Map<String, OneOfGroup> oneOfGroups;
 
@@ -535,6 +536,11 @@ public class YamlValidator {
         }
         if (canonical) {
             checkOneOfCardinality(target, new NodePath(PathType.JSON_POINTER), 
errors);
+            // unmarshal: {jackson: {}}: the unknown key already got its hint; 
the list of every data format that
+            // "found none" adds at the same location only buries it
+            errors.removeIf(e -> "oneOf".equals(e.getKeyword())
+                    && hinted.contains(String.valueOf(e.getInstanceLocation()))
+                    && String.valueOf(e.getMessage()).endsWith("but found 
none"));
         }
         return errors;
     }
@@ -895,7 +901,7 @@ public class YamlValidator {
      */
     private Map<String, OneOfGroup> loadOneOfGroups() {
         Map<String, OneOfGroup> groups = new HashMap<>();
-        CamelCatalog catalog = this.catalog != null ? this.catalog : new 
DefaultCamelCatalog();
+        CamelCatalog catalog = catalog();
         for (String name : catalog.findModelNames()) {
             EipModel model = catalog.eipModel(name);
             if (model == null) {
@@ -999,6 +1005,43 @@ public class YamlValidator {
         return answer;
     }
 
+    /**
+     * The hint for a marshal/unmarshal key that is not a data format key: the 
key spelled as the schema has it
+     * (jackson-xml: jacksonXml), the data format named as its artifact or 
catalog entry (jackson, json-jackson: json
+     * with library Jackson), the catalog's suggestions for a word of a name 
(xml: jacksonXml, fhirXml, groovyXml), the
+     * closest key for a typo (jsn: json), and failing all that, what the key 
is.
+     */
+    String dataFormatHint(String unknown, String eip, String schemaLocation) {
+        Set<String> keys = knownProperties(schemaLocation);
+        String hint = DataFormatKeyHints.hint(unknown, keys);
+        if (hint != null) {
+            return hint;
+        }
+        List<String> names = catalog().suggestDataFormatNames(unknown, 3);
+        List<String> forms = 
names.stream().map(DataFormatKeyHints::form).distinct().toList();
+        if (forms.size() == 1) {
+            hint = DataFormatKeyHints.hint(names.get(0), keys);
+            return hint != null ? hint : "did you mean '" + forms.get(0) + 
"'?";
+        }
+        if (forms.size() > 1) {
+            return "did you mean " + String.join(", ", forms.subList(0, 
forms.size() - 1)) + " or "
+                   + forms.get(forms.size() - 1) + "?";
+        }
+        String closest = closest(unknown, keys);
+        if (closest != null) {
+            return "did you mean '" + closest + "'?";
+        }
+        return "the key of " + eip + " is the data format: json, jacksonXml, 
csv, yaml, jaxb, avro, protobuf...;"
+               + " camel catalog dataformat lists them";
+    }
+
+    private CamelCatalog catalog() {
+        if (catalog == null) {
+            catalog = new DefaultCamelCatalog();
+        }
+        return catalog;
+    }
+
     private void collectProperties(JsonNode node, Set<String> answer, int 
depth) {
         if (node == null || node.isMissingNode() || depth > 3) {
             return;
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/DataFormatKeyHintsCatalogTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/DataFormatKeyHintsCatalogTest.java
new file mode 100644
index 000000000000..5ba50f9425b9
--- /dev/null
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/DataFormatKeyHintsCatalogTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.yaml.common.DataFormatKeyHints;
+import org.apache.camel.tooling.model.DataFormatModel;
+import org.apache.camel.tooling.model.EipModel;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-24847: the alias table the runtime deserializer uses without a 
catalog must agree with the catalog: every data
+ * format whose name differs from its model name is in it, with the model name 
as the key and a value of the option that
+ * selects the library or type.
+ */
+public class DataFormatKeyHintsCatalogTest {
+
+    private final CamelCatalog catalog = new DefaultCamelCatalog();
+
+    @Test
+    public void testEveryAliasedDataFormatOfTheCatalogIsInTheTable() {
+        List<String> aliased = new ArrayList<>();
+        for (String name : catalog.findDataFormatNames()) {
+            DataFormatModel df = catalog.dataFormatModel(name);
+            if (df.getModelName().equals(name)) {
+                assertThat(DataFormatKeyHints.alias(name)).as(name + " is its 
own key").isNull();
+                continue;
+            }
+            aliased.add(name);
+            DataFormatKeyHints.Alias alias = DataFormatKeyHints.alias(name);
+            assertThat(alias).as(name + " maps to " + 
df.getModelName()).isNotNull();
+            assertThat(alias.key()).as(name).isEqualTo(df.getModelName());
+            EipModel model = catalog.eipModel(df.getModelName());
+            EipModel.EipOptionModel selector = model.getOptions().stream()
+                    .filter(o -> o.getEnums() != null && o.getEnums().size() > 
1
+                            && (o.getName().equals("library") || 
o.getName().equals("type")))
+                    .findFirst().orElse(null);
+            if (selector == null) {
+                assertThat(alias.option()).as(name + " has no library or type 
to select").isNull();
+            } else {
+                
assertThat(alias.option()).as(name).isEqualTo(selector.getName());
+                assertThat(selector.getEnums()).as(name + " " + 
selector.getName()).contains(alias.value());
+            }
+        }
+        // and nothing in the table that the catalog does not have: a 
misspelled key would keep the sizes equal
+        assertThat(DataFormatKeyHints.ALIASES.keySet()).as("every alias is the 
normalized name of a catalog data format")
+                
.isSubsetOf(aliased.stream().map(DataFormatKeyHints::normalize).collect(Collectors.toSet()));
+        assertThat(DataFormatKeyHints.ALIASES).as("aliases without a catalog 
data format: " + aliased)
+                .hasSize(aliased.size());
+    }
+
+    @Test
+    public void testSpellingsOfAnAlias() {
+        
assertThat(DataFormatKeyHints.alias("jackson")).isEqualTo(DataFormatKeyHints.ALIASES.get("jackson"));
+        
assertThat(DataFormatKeyHints.alias("Jackson")).isEqualTo(DataFormatKeyHints.ALIASES.get("jackson"));
+        
assertThat(DataFormatKeyHints.alias("json-jackson")).isEqualTo(DataFormatKeyHints.ALIASES.get("jackson"));
+        
assertThat(DataFormatKeyHints.alias("jackson-json")).isEqualTo(DataFormatKeyHints.ALIASES.get("jackson"));
+        
assertThat(DataFormatKeyHints.alias("jackson-avro")).isEqualTo(DataFormatKeyHints.ALIASES.get("avrojackson"));
+        
assertThat(DataFormatKeyHints.alias("bindy_csv")).isEqualTo(DataFormatKeyHints.ALIASES.get("bindycsv"));
+        
assertThat(DataFormatKeyHints.alias("snakeYaml")).isEqualTo(DataFormatKeyHints.ALIASES.get("snakeyaml"));
+        // a library of another model is not an alias of this one
+        assertThat(DataFormatKeyHints.alias("yaml-jackson")).isNull();
+        assertThat(DataFormatKeyHints.alias("json")).isNull();
+        assertThat(DataFormatKeyHints.alias("jacksonXml")).isNull();
+        assertThat(DataFormatKeyHints.form("gson")).isEqualTo("json: {library: 
Gson}");
+        
assertThat(DataFormatKeyHints.form("jacksonXml")).isEqualTo("jacksonXml");
+    }
+}
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
index f4b502bfc9a9..bd8af78f8a48 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
@@ -631,6 +631,85 @@ public class YamlValidatorPropertyHintTest {
         assertThat(errors.get(0).getMessage()).contains("no YAML").contains("- 
route:");
     }
 
+    // CAMEL-24847: a data format named as its artifact or catalog entry says 
which YAML key and option to write
+
+    private static String unmarshalError(YamlValidator v, String key) throws 
Exception {
+        List<Error> errors = v.validate("""
+                - from:
+                    uri: timer:tick
+                    steps:
+                      - unmarshal:
+                          %s: {}
+                """.formatted(key));
+        assertThat(errors).as(key).hasSize(1);
+        return errors.get(0).getMessage();
+    }
+
+    @Test
+    public void testDataFormatLibraryNameSaysTheKeyAndTheLibrary() throws 
Exception {
+        assertThat(unmarshalError(validator, "jackson"))
+                .contains("property 'jackson' is not defined")
+                .contains("the data format is json, Jackson is its library: 
write json: {library: Jackson}");
+        assertThat(unmarshalError(validator, "gson")).contains("write json: 
{library: Gson}");
+        assertThat(unmarshalError(validator, "json-b")).contains("write json: 
{library: Jsonb}");
+        assertThat(unmarshalError(validator, 
"protobuf-jackson")).contains("write protobuf: {library: Jackson}");
+        assertThat(unmarshalError(validator, "jackson-avro")).contains("write 
avro: {library: Jackson}");
+        assertThat(unmarshalError(validator, "bindy-csv"))
+                .contains("the data format is bindy, Csv is its type: write 
bindy: {type: Csv}");
+        assertThat(unmarshalError(validator, "snake-yaml")).contains("the data 
format is yaml: write yaml: {...}");
+    }
+
+    @Test
+    public void testDataFormatArtifactNameSaysTheKeyAndTheLibrary() throws 
Exception {
+        // json-jackson is the artifact, not a data format name: the catalog's 
suggestion is jackson
+        assertThat(unmarshalError(validator, "json-jackson"))
+                .contains("the data format is json, Jackson is its library: 
write json: {library: Jackson}");
+    }
+
+    @Test
+    public void testDataFormatKeySpelledDifferentlyGetsTheKey() throws 
Exception {
+        assertThat(unmarshalError(validator, "jackson-xml")).contains("did you 
mean 'jacksonXml'?");
+        assertThat(unmarshalError(validator, "JSON")).contains("did you mean 
'json'?");
+        assertThat(unmarshalError(validator, "base-64")).contains("did you 
mean 'base64'?");
+    }
+
+    @Test
+    public void testDataFormatWordListsTheCatalogMatches() throws Exception {
+        assertThat(unmarshalError(validator, "xml")).contains("did you mean 
fhirXml, groovyXml or jacksonXml?");
+        assertThat(unmarshalError(validator, "zip")).contains("did you mean 
zipDeflater, zipFile or gzipDeflater?");
+        assertThat(unmarshalError(validator, "gzip")).contains("did you mean 
'gzipDeflater'?");
+    }
+
+    @Test
+    public void testDataFormatTypoGetsTheClosestKey() throws Exception {
+        assertThat(unmarshalError(validator, "jsn")).contains("did you mean 
'json'?");
+        assertThat(unmarshalError(validator, "yml")).contains("did you mean 
'yaml'?");
+    }
+
+    @Test
+    public void testUnknownDataFormatSaysWhatTheKeyIs() throws Exception {
+        assertThat(unmarshalError(validator, "xstream"))
+                .contains("the key of unmarshal is the data format: json, 
jacksonXml, csv, yaml")
+                .contains("camel catalog dataformat");
+    }
+
+    @Test
+    public void 
testCanonicalDataFormatHintIsNotFollowedByTheListOfEveryDataFormat() throws 
Exception {
+        YamlValidator canonical = new YamlValidator(true);
+        List<Error> errors = canonical.validate("""
+                - route:
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - marshal:
+                            json-jackson: {}
+                """);
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0).getMessage())
+                .contains("write json: {library: Jackson}")
+                .doesNotContain("must have exactly one of");
+    }
+
     @Test
     public void testDistance() {
         assertThat(YamlValidator.distance("loggername", 
"logname")).isEqualTo(3);
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/UnmarshalTest.groovy
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/UnmarshalTest.groovy
index 631b38ea57fd..e39335db16dc 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/UnmarshalTest.groovy
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/UnmarshalTest.groovy
@@ -20,6 +20,7 @@ import org.apache.camel.dsl.yaml.support.YamlTestSupport
 import org.apache.camel.model.UnmarshalDefinition
 import org.apache.camel.spi.Resource
 import org.apache.camel.support.PluginHelper
+import org.apache.camel.support.ResourceHelper
 
 class UnmarshalTest extends YamlTestSupport {
 
@@ -128,4 +129,33 @@ class UnmarshalTest extends YamlTestSupport {
                 'true', 'false', null
         ]
     }
+
+    // CAMEL-24847: a data format named as its artifact or catalog entry says 
which key and option to write
+    def "#eip with #key fails with a message naming the data format 
key"(String eip, String key, String hint) {
+        when:
+            loadRoutes([ResourceHelper.fromString("route-1.yaml", """
+                - from:
+                    uri: timer:tick
+                    steps:
+                      - ${eip}:
+                          ${key}: {}
+            """.stripIndent())], false)
+        then:
+            def e = thrown(Exception)
+            def messages = []
+            for (Throwable t = e; t != null; t = t.cause) {
+                messages << t.message
+            }
+            messages.any { it != null && it.contains("Error constructing YAML 
node id: ${eip}: unsupported field: ${key}") && it.contains(hint) }
+        where:
+            eip         | key            | hint
+            'unmarshal' | 'jackson'      | 'the data format is json, Jackson 
is its library: write json: {library: Jackson}'
+            'unmarshal' | 'json-jackson' | 'write json: {library: Jackson}'
+            'unmarshal' | 'gson'         | 'write json: {library: Gson}'
+            'unmarshal' | 'bindy-csv'    | 'the data format is bindy, Csv is 
its type: write bindy: {type: Csv}'
+            'unmarshal' | 'snake-yaml'   | 'the data format is yaml: write 
yaml: {...}'
+            'unmarshal' | 'JSON'         | "did you mean 'json'?"
+            'marshal'   | 'jackson'      | 'the data format is json, Jackson 
is its library: write json: {library: Jackson}'
+            'marshal'   | 'JSON'         | "did you mean 'json'?"
+    }
 }

Reply via email to