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'?"
+ }
}