You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/10/19 11:46:43 UTC
(camel) branch main updated: CAMEL-24704: camel-yaml-dsl - beans keyed by the bean name fail with the list form to write (#26921)
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 04333431a654 CAMEL-24704: camel-yaml-dsl - beans keyed by the bean name fail with the list form to write (#26921)
04333431a654 is described below
commit 04333431a654883ffd1694ebe34b80ad2c393737
Author: Adriano Machado <60...@users.noreply.github.com>
AuthorDate: Mon Oct 19 07:46:36 2026 -0400
CAMEL-24704: camel-yaml-dsl - beans keyed by the bean name fail with the list form to write (#26921)
* CAMEL-24704: camel-yaml-dsl - beans keyed by the bean name fail with the list form to write
* CAMEL-24704: camel-yaml-dsl-validator - the bean item hints also cover routeTemplate and templatedRoute
Co-authored-by: Claude Opus 5.5 <no...@anthropic.com>
---
.../dsl/yaml/deserializers/BeansDeserializer.java | 41 +++++++++++
.../RouteTemplateDefinitionDeserializer.java | 1 +
.../TemplatedRouteDefinitionDeserializer.java | 1 +
.../camel/dsl/yaml/validator/SchemaHints.java | 4 +-
.../validator/YamlValidatorPropertyHintTest.java | 39 ++++++++++
.../java/org/apache/camel/dsl/yaml/BeansTest.java | 83 ++++++++++++++++++++++
6 files changed, 167 insertions(+), 2 deletions(-)
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/BeansDeserializer.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/BeansDeserializer.java
index 92b4ef071ea1..46c58236ec00 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/BeansDeserializer.java
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/BeansDeserializer.java
@@ -27,6 +27,7 @@ import org.apache.camel.CamelContext;
import org.apache.camel.dsl.yaml.common.YamlDeserializationContext;
import org.apache.camel.dsl.yaml.common.YamlDeserializerResolver;
import org.apache.camel.dsl.yaml.common.YamlDeserializerSupport;
+import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException;
import org.apache.camel.model.BeanFactoryDefinition;
import org.apache.camel.model.BeanModelHelper;
import org.apache.camel.model.Model;
@@ -39,7 +40,10 @@ import org.apache.camel.support.PluginHelper;
import org.apache.camel.support.PojoBeanHelper;
import org.apache.camel.util.ObjectHelper;
import org.snakeyaml.engine.v2.api.ConstructNode;
+import org.snakeyaml.engine.v2.nodes.MappingNode;
import org.snakeyaml.engine.v2.nodes.Node;
+import org.snakeyaml.engine.v2.nodes.NodeTuple;
+import org.snakeyaml.engine.v2.nodes.ScalarNode;
import org.snakeyaml.engine.v2.nodes.SequenceNode;
@YamlIn
@@ -57,6 +61,7 @@ public class BeansDeserializer extends YamlDeserializerSupport implements Constr
@Override
public Object construct(Node node) {
final BeansCustomizer answer = new BeansCustomizer();
+ checkBeansIsAList(node);
final SequenceNode sn = asSequenceNode(node);
final YamlDeserializationContext dc = getDeserializationContext(node);
@@ -100,6 +105,42 @@ public class BeansDeserializer extends YamlDeserializerSupport implements Constr
beanCache.clear();
}
+ /**
+ * Fails with what to write when beans: is written keyed by the bean name, as a map (beans: {myBean: {type: ...}})
+ * or as list items (- myBean: {type: ...}), which is what people and AI models tend to write first (CAMEL-24704).
+ * The list with name: is the only form of beans: (also in routeTemplate and templatedRoute).
+ */
+ static void checkBeansIsAList(Node node) {
+ if (node instanceof MappingNode mn) {
+ String name = "myBean";
+ if (!mn.getValue().isEmpty()) {
+ NodeTuple first = mn.getValue().get(0);
+ String key = first.getKeyNode() instanceof ScalarNode sk ? sk.getValue() : name;
+ // beans:\n name: myBean\n type: ...: the "- " of the list item was forgotten
+ if ("name".equals(key) && first.getValueNode() instanceof ScalarNode sv) {
+ key = sv.getValue();
+ }
+ name = key;
+ }
+ throw new YamlDeserializationException(
+ node, "beans is a list: each bean starts with \"- \": - name: " + name
+ + " followed by type: \"#class:com.example.MyBean\" (indented under the -)");
+ }
+ if (node instanceof SequenceNode sn) {
+ for (Node item : sn.getValue()) {
+ // - myBean: {type: ...}: a single key holding the bean, which is not one of its map properties
+ if (item instanceof MappingNode mn && mn.getValue().size() == 1
+ && mn.getValue().get(0).getKeyNode() instanceof ScalarNode sk
+ && mn.getValue().get(0).getValueNode() instanceof MappingNode
+ && !Set.of("properties", "constructors").contains(sk.getValue())) {
+ throw new YamlDeserializationException(
+ item, "a bean item is written as - name: " + sk.getValue()
+ + " followed by type: \"#class:com.example.MyBean\" (the name is a property, not the key)");
+ }
+ }
+ }
+ }
+
protected void registerBean(
CamelContext camelContext,
List<BeanFactoryDefinition<?>> delayedRegistrations,
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/RouteTemplateDefinitionDeserializer.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/RouteTemplateDefinitionDeserializer.java
index 2322b47349f3..230304184693 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/RouteTemplateDefinitionDeserializer.java
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/RouteTemplateDefinitionDeserializer.java
@@ -100,6 +100,7 @@ public class RouteTemplateDefinitionDeserializer extends YamlDeserializerBase<Ro
break;
}
case "beans": {
+ BeansDeserializer.checkBeansIsAList(node);
List<BeanFactoryDefinition<RouteTemplateDefinition>> items
= (List) asFlatList(node, BeanFactoryDefinition.class);
target.setTemplateBeans(items);
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/TemplatedRouteDefinitionDeserializer.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/TemplatedRouteDefinitionDeserializer.java
index 93f4f62a4cbc..8341ca666a7b 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/TemplatedRouteDefinitionDeserializer.java
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/TemplatedRouteDefinitionDeserializer.java
@@ -88,6 +88,7 @@ public class TemplatedRouteDefinitionDeserializer extends YamlDeserializerBase<T
break;
}
case "beans": {
+ BeansDeserializer.checkBeansIsAList(node);
List<BeanFactoryDefinition<TemplatedRouteDefinition>> items
= (List) asFlatList(node, BeanFactoryDefinition.class);
target.setBeans(items);
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 d8ad480acf0a..f73aaf52c2e9 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
@@ -470,11 +470,11 @@ final class SchemaHints {
m -> "steps: is a property of from:, next to uri:; a steps: written at the route level must be"
+ " indented under from:"),
// - id: myBean / class: ... : the bean properties are name and type
- unknownProperty("/\\d+/beans/\\d+", m -> Set.of("id", "ref", "class").contains(m.unknown()),
+ unknownProperty(".*/beans/\\d+", m -> Set.of("id", "ref", "class").contains(m.unknown()),
m -> "a bean is - name: myBean followed by type: \"#class:com.example.MyBean\" (name instead of "
+ m.unknown() + (m.unknown().equals("class") ? ", type instead of class" : "") + ")"),
// - myBean: {type: ...} instead of - name: myBean / type: ...
- unknownProperty("/\\d+/beans/\\d+", ANY,
+ unknownProperty(".*/beans/\\d+", ANY,
m -> "a bean item is written as - name: " + m.unknown()
+ " followed by type: \"#class:com.example.MyBean\" (the name is a property, not the key)"),
unknownProperty(null,
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 7def87e4ed95..dbeb8e2d6a8a 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
@@ -428,6 +428,45 @@ public class YamlValidatorPropertyHintTest {
assertThat(errors).anyMatch(e -> e.getMessage().contains("a bean item is written as - name: myBean"));
}
+ @Test
+ public void testTemplateBeansKeyedByNameSayHowToWriteThem() throws Exception {
+ // CAMEL-24704: the beans of a routeTemplate and a templatedRoute are the same list as the top-level beans
+ List<Error> errors = validator.validate("""
+ - routeTemplate:
+ id: myTemplate
+ beans:
+ myBean:
+ type: "#class:com.example.MyBean"
+ from:
+ uri: "direct:{{name}}"
+ steps:
+ - to: "mock:result"
+ """);
+ assertThat(errors).anyMatch(e -> e.getMessage().contains("beans is a list: - name: myBean"));
+
+ errors = validator.validate("""
+ - templatedRoute:
+ routeTemplateRef: myTemplate
+ beans:
+ - myBean:
+ type: "#class:com.example.MyBean"
+ """);
+ assertThat(errors).anyMatch(e -> e.getMessage().contains("a bean item is written as - name: myBean"));
+
+ errors = validator.validate("""
+ - routeTemplate:
+ id: myTemplate
+ beans:
+ - id: myBean
+ type: "#class:com.example.MyBean"
+ from:
+ uri: "direct:{{name}}"
+ steps:
+ - to: "mock:result"
+ """);
+ assertThat(errors).anyMatch(e -> e.getMessage().contains("(name instead of id)"));
+ }
+
@Test
public void testJbangDepsLineIsNamed() throws Exception {
List<Error> errors = validator.validate("""
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/BeansTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/BeansTest.java
index 207f756a1b80..d99692b595d0 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/BeansTest.java
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/BeansTest.java
@@ -406,6 +406,89 @@ class BeansTest extends YamlTestSupport {
assertThat(msg.contains("did you mean")).isFalse();
}
+ // CAMEL-24704: beans keyed by the bean name, as most other formats write them, fail with the list form to write
+ // instead of "Node type map is invalid, expected array" / "Unsupported field: myProcessor"
+
+ @Test
+ void beansAsMapSaysBeansIsAList() {
+ Exception e = assertThrows(Exception.class, () -> loadRoutesNoValidate("""
+ - beans:
+ myProcessor:
+ type: %s
+ """.formatted(MyBean.class.getName())));
+
+ assertThat(messages(e)).contains("beans is a list: each bean starts with \"- \": - name: myProcessor");
+ }
+
+ @Test
+ void beansListWithoutDashSaysBeansIsAList() {
+ // the "- " was forgotten: the bean reads as a map whose first key is name, so the hint uses its value
+ Exception e = assertThrows(Exception.class, () -> loadRoutesNoValidate("""
+ - beans:
+ name: myProcessor
+ type: %s
+ """.formatted(MyBean.class.getName())));
+
+ assertThat(messages(e)).contains("beans is a list: each bean starts with \"- \": - name: myProcessor");
+ }
+
+ @Test
+ void beanItemKeyedByNameSaysNameIsAProperty() {
+ Exception e = assertThrows(Exception.class, () -> loadRoutesNoValidate("""
+ - beans:
+ - name: first
+ type: %s
+ - myProcessor:
+ type: %s
+ """.formatted(MyBean.class.getName(), MyBean.class.getName())));
+
+ assertThat(messages(e))
+ .contains("a bean item is written as - name: myProcessor")
+ .contains("(the name is a property, not the key)");
+ }
+
+ @Test
+ void beanItemWithOnlyPropertiesIsNotReadAsKeyedByName() {
+ // properties: is a map property of the bean, not its name: the error is the missing name
+ Exception e = assertThrows(Exception.class, () -> loadRoutesNoValidate("""
+ - beans:
+ - properties:
+ field1: f1
+ """));
+
+ assertThat(messages(e)).contains("The bean name must be set").doesNotContain("a bean item is written as");
+ }
+
+ @Test
+ void routeTemplateBeansAsMapSaysBeansIsAList() {
+ Exception e = assertThrows(Exception.class, () -> loadRoutesNoValidate("""
+ - routeTemplate:
+ id: myTemplate
+ beans:
+ myProcessor:
+ type: %s
+ from:
+ uri: "direct:{{directName}}"
+ steps:
+ - to: "mock:result"
+ """.formatted(MyBean.class.getName())));
+
+ assertThat(messages(e)).contains("beans is a list: each bean starts with \"- \": - name: myProcessor");
+ }
+
+ @Test
+ void templatedRouteBeanItemKeyedByNameSaysNameIsAProperty() {
+ Exception e = assertThrows(Exception.class, () -> loadRoutesNoValidate("""
+ - templatedRoute:
+ routeTemplateRef: myTemplate
+ beans:
+ - myProcessor:
+ type: %s
+ """.formatted(MyBean.class.getName())));
+
+ assertThat(messages(e)).contains("a bean item is written as - name: myProcessor");
+ }
+
private static String messages(Throwable e) {
StringBuilder sb = new StringBuilder();
for (Throwable t = e; t != null; t = t.getCause()) {