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()) {