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 2023/06/08 11:55:41 UTC

[camel] 03/03: CAMEL-19307: camel-yaml-io - dumping routes to yaml

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

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

commit 67361bd2a3d3e0dc1f0467d9f795f208667e4bdc
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Thu Jun 8 13:55:12 2023 +0200

    CAMEL-19307: camel-yaml-io - dumping routes to yaml
---
 core/camel-xml-io/src/test/resources/loop.xml      |   3 +-
 core/camel-yaml-io/pom.xml                         |   5 +
 .../org/apache/camel/yaml/LwModelToYAMLDumper.java |   3 +-
 .../java/org/apache/camel/yaml/io/EipNode.java     | 248 +----------
 .../apache/camel/yaml/io/NewJacksonYamlWriter.java | 355 ----------------
 .../org/apache/camel/yaml/io/NewYamlWriter.java    | 280 -------------
 .../java/org/apache/camel/yaml/io/YamlWriter.java  | 451 +++++++++++++++------
 .../java/org/apache/camel/yaml/out/BaseWriter.java |   6 +-
 .../org/apache/camel/yaml/out/ModelWriterTest.java |  56 +++
 .../org/apache/camel/yaml/out/XmlToYamlTest.java   |  78 ++++
 core/camel-yaml-io/src/test/resources/route8.yaml  |  20 +
 core/camel-yaml-io/src/test/resources/route9.yaml  |   9 +
 12 files changed, 510 insertions(+), 1004 deletions(-)

diff --git a/core/camel-xml-io/src/test/resources/loop.xml b/core/camel-xml-io/src/test/resources/loop.xml
index 9aa92142370..8c0e7645a5d 100644
--- a/core/camel-xml-io/src/test/resources/loop.xml
+++ b/core/camel-xml-io/src/test/resources/loop.xml
@@ -19,9 +19,10 @@
 -->
 <routes id="camel" xmlns="http://camel.apache.org/schema/spring">
   <route>
-    <from uri="direct:a"/>
+    <from uri="timer:a"/>
     <loop>
       <constant>8</constant>
+      <log message="looping"/>
     </loop>
   </route>
 </routes>
diff --git a/core/camel-yaml-io/pom.xml b/core/camel-yaml-io/pom.xml
index 2cb6a70214a..df414c9b87f 100644
--- a/core/camel-yaml-io/pom.xml
+++ b/core/camel-yaml-io/pom.xml
@@ -57,6 +57,11 @@
         </dependency>
 
         <!-- testing -->
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-xml-io</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter</artifactId>
diff --git a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java
index daa26c58d88..d111e5c27b1 100644
--- a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java
+++ b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java
@@ -53,6 +53,7 @@ import static org.apache.camel.model.ProcessorDefinitionHelper.filterTypeInOutpu
  */
 @JdkService(ModelToYAMLDumper.FACTORY)
 public class LwModelToYAMLDumper implements ModelToYAMLDumper {
+
     @Override
     public String dumpModelAsYaml(CamelContext context, NamedNode definition) throws Exception {
         return dumpModelAsYaml(context, definition, false, false);
@@ -208,7 +209,7 @@ public class LwModelToYAMLDumper implements ModelToYAMLDumper {
 
     /**
      * If the route has been built with endpoint-dsl, then the model will not have uri set which then cannot be included
-     * in the JAXB model dump
+     * in the model dump
      */
     @SuppressWarnings("rawtypes")
     private static void resolveEndpointDslUris(RouteDefinition route) {
diff --git a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/EipNode.java b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/EipNode.java
index 5abe8e72171..df70acc4d9f 100644
--- a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/EipNode.java
+++ b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/EipNode.java
@@ -22,7 +22,6 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
-import org.apache.camel.util.StringHelper;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 
@@ -31,7 +30,7 @@ import org.apache.camel.util.json.JsonObject;
  */
 class EipNode {
 
-    private String name;
+    private final String name;
     private final EipNode parent;
     private final boolean output;
     private final boolean expression;
@@ -48,10 +47,6 @@ class EipNode {
         this.expression = expression;
     }
 
-    public void clearName() {
-        this.name = null;
-    }
-
     public String getName() {
         return name;
     }
@@ -126,231 +121,15 @@ class EipNode {
         properties.put(key, value);
     }
 
-    public String dump(int level, int spaces, String lineSeparator, boolean steps) {
-        if ("choice".equals(name)) {
-            // special for choice EIP
-            return dumpChoice(level, spaces, lineSeparator);
-        }
-
-        StringBuilder sb = new StringBuilder();
-        String n = name;
-
-        if (n != null && steps) {
-            n = "- " + name;
-        }
-        if (n != null) {
-            sb.append(padString(spaces, level)).append(n).append(":");
-            if (n.startsWith("- ")) {
-                level++;
-            }
-        }
-        if (expressions != null) {
-            level++;
-            for (EipNode child : expressions) {
-                if (child.getExpressions().isEmpty()) {
-                    sb.append(lineSeparator).append(padString(spaces, level)).append("expression:");
-                    level++;
-                }
-                String text = child.dumpExpression(level, spaces, lineSeparator);
-                sb.append("\n");
-                sb.append(text);
-                if (child.getExpressions().isEmpty()) {
-                    level--;
-                }
-            }
-            level--;
-        }
-        if (text != null) {
-            String escaped = escape(spaces, level + 2, lineSeparator, text);
-            sb.append(lineSeparator).append(padString(spaces, level + 1)).append("expression: ").append(escaped);
-        }
-        if (properties != null) {
-            for (Map.Entry<String, Object> entry : properties.entrySet()) {
-                String escaped = escape(spaces, level + 1, lineSeparator, entry.getValue());
-                String line = String.format("%s: %s", entry.getKey(), escaped);
-                sb.append(lineSeparator).append(padString(spaces, level + 1)).append(line);
-            }
-        }
-        if (input != null) {
-            String text = input.dump(level + 1, spaces, lineSeparator, false);
-            sb.append(lineSeparator);
-            sb.append(text);
-            level++;
-        }
-        if (outputs != null) {
-            level++;
-            sb.append("\n").append(padString(spaces, level)).append("steps:");
-            level++;
-            for (EipNode child : outputs) {
-                String text = child.dump(level, spaces, lineSeparator, true);
-                sb.append("\n");
-                sb.append(text);
-            }
-        }
-        return sb.toString();
-    }
-
-    public String dumpExpression(int level, int spaces, String lineSeparator) {
-        StringBuilder sb = new StringBuilder();
-        String n = name;
-        sb.append(padString(spaces, level)).append(n).append(":");
-        if (n.startsWith("- ")) {
-            level++;
-        }
-        if (text != null) {
-            String escaped = escape(spaces, level + 1, lineSeparator, text);
-            sb.append(lineSeparator).append(padString(spaces, level + 1)).append("expression: ").append(escaped);
-        }
-        if (properties != null) {
-            for (Map.Entry<String, Object> entry : properties.entrySet()) {
-                String escaped = escape(spaces, level + 1, lineSeparator, entry.getValue());
-                String line = String.format("%s: %s", entry.getKey(), escaped);
-                sb.append(lineSeparator).append(padString(spaces, level + 1)).append(line);
-            }
-        }
-        if (expressions != null) {
-            level++;
-            for (EipNode child : expressions) {
-                String text = child.dumpExpression(level, spaces, lineSeparator);
-                sb.append("\n");
-                sb.append(text);
-            }
-        }
-        return sb.toString();
-    }
-
-    public String dumpChoice(int level, int spaces, String lineSeparator) {
-        StringBuilder sb = new StringBuilder();
-
-        sb.append(padString(spaces, level)).append("- choice").append(":");
-        level++;
-        if (properties != null) {
-            for (Map.Entry<String, Object> entry : properties.entrySet()) {
-                String escaped = escape(spaces, level + 1, lineSeparator, entry.getValue());
-                String line = String.format("%s: %s", entry.getKey(), escaped);
-                sb.append(lineSeparator).append(padString(spaces, level + 1)).append(line);
-            }
-        }
-        // sort so otherwise is last
-        if (outputs != null) {
-            outputs.sort((o1, o2) -> {
-                if ("otherwise".equals(o1.name)) {
-                    return 1;
-                } else if ("otherwise".equals(o2.name)) {
-                    return -1;
-                }
-                return 0;
-            });
-            // when
-            for (int i = 0; i < outputs.size(); i++) {
-                EipNode child = outputs.get(i);
-                if (i == 0 || "otherwise".equals(child.name)) {
-                    level++;
-                    sb.append(lineSeparator).append(padString(spaces, level)).append(child.name).append(":");
-                    level++;
-                }
-                child.clearName();
-                String text = child.dump(level, spaces, lineSeparator, true);
-                sb.append("\n");
-                sb.append(text);
-            }
-        }
-        return sb.toString();
-    }
-
-    private static String padString(int spaces, int level) {
-        return " ".repeat(spaces).repeat(level);
-    }
-
     @Override
     public String toString() {
         return name;
     }
 
-    private static String escape(int spaces, int level, String lineSeparator, Object value) {
-        // escapes the yaml
-        StringBuilder sb = new StringBuilder();
-
-        String text = value.toString();
-
-        if (text.contains(lineSeparator)) {
-            // multi lined values
-            String[] lines = text.split(lineSeparator);
-            sb.append("|-");
-            for (String line : lines) {
-                sb.append(lineSeparator).append(padString(spaces, level + 1)).append(line);
-            }
-        } else if (text.contains("\\") || text.contains("//")) {
-            // using slash/backslashes
-            sb.append(">-");
-            sb.append(lineSeparator).append(padString(spaces, level + 1)).append(text);
-        } else if (StringHelper.isSingleQuoted(text)) {
-            // single quotes should be escaped to triple
-            sb.append("''");
-            sb.append(text);
-            sb.append("''");
-        } else if (StringHelper.isDoubleQuoted(text)) {
-            // double quotes should be enclosed by single quote
-            sb.append("'");
-            // single quotes should be escaped
-            text = text.replace("'", "''");
-            sb.append(text);
-            sb.append("'");
-        } else if (valueHasQuotableChar(text)) {
-            // should be enclosed by single quote
-            sb.append("'");
-            // single quotes should be escaped
-            text = text.replace("'", "''");
-            sb.append(text);
-            sb.append("'");
-        } else {
-            sb.append(text);
-        }
-
-        return sb.toString();
-    }
-
     /**
-     * As per YAML <a href="https://yaml.org/spec/1.2/spec.html#id2788859">Plain Style</a>unquoted strings are
-     * restricted to a reduced charset and must be quoted in case they contain one of the following characters or
-     * character combinations.
+     * Converts this node to JSon
      */
-    private static boolean valueHasQuotableChar(String inputStr) {
-        // https://github.com/FasterXML/jackson-dataformats-text/blob/2.16/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/util/StringQuotingChecker.java
-        final int end = inputStr.length();
-        for (int i = 0; i < end; ++i) {
-            switch (inputStr.charAt(i)) {
-                case '[':
-                case ']':
-                case '{':
-                case '}':
-                case ',':
-                    return true;
-                case '#':
-                    // [dataformats-text#201]: limit quoting with MINIMIZE_QUOTES
-                    if (i > 0) {
-                        char d = inputStr.charAt(i - 1);
-                        if (' ' == d || '\t' == d) {
-                            return true;
-                        }
-                    }
-                    break;
-                case ':':
-                    // [dataformats-text#201]: limit quoting with MINIMIZE_QUOTES
-                    if (i < (end - 1)) {
-                        char d = inputStr.charAt(i + 1);
-                        if (' ' == d || '\t' == d) {
-                            return true;
-                        }
-                    }
-                    break;
-                default:
-            }
-        }
-        return false;
-    }
-
-    public JsonObject asJsonObject() {
+    JsonObject asJsonObject() {
         JsonObject answer = new JsonObject();
 
         if (properties != null) {
@@ -376,7 +155,6 @@ class EipNode {
                 }
             }
         }
-
         if (outputs != null) {
             // sort so otherwise is last
             outputs.sort((o1, o2) -> {
@@ -387,14 +165,20 @@ class EipNode {
                 }
                 return 0;
             });
-            JsonArray arr = new JsonArray();
-            for (EipNode o : outputs) {
-                JsonObject r = o.asJsonObject();
-                JsonObject wrap = new JsonObject();
-                wrap.put(o.getName(), r);
-                arr.add(wrap);
+            if (("marshal".equals(name) || "unmarshal".equals(name)) && outputs.size() == 1) {
+                EipNode o = outputs.get(0);
+                JsonObject jo = o.asJsonObject();
+                answer.put(o.getName(), jo);
+            } else {
+                JsonArray arr = new JsonArray();
+                for (EipNode o : outputs) {
+                    JsonObject r = o.asJsonObject();
+                    JsonObject wrap = new JsonObject();
+                    wrap.put(o.getName(), r);
+                    arr.add(wrap);
+                }
+                answer.put("steps", arr);
             }
-            answer.put("steps", arr);
         }
 
         return answer;
diff --git a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/NewJacksonYamlWriter.java b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/NewJacksonYamlWriter.java
deleted file mode 100644
index 8fb9cfb4be8..00000000000
--- a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/NewJacksonYamlWriter.java
+++ /dev/null
@@ -1,355 +0,0 @@
-/*
- * 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.yaml.io;
-
-import java.io.IOException;
-import java.io.Writer;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Stack;
-import java.util.StringJoiner;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
-import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
-import org.apache.camel.catalog.impl.DefaultRuntimeCamelCatalog;
-import org.apache.camel.tooling.model.BaseOptionModel;
-import org.apache.camel.tooling.model.EipModel;
-import org.apache.camel.util.json.JsonArray;
-import org.apache.camel.util.json.JsonObject;
-
-public class NewJacksonYamlWriter {
-
-    private final Writer writer;
-    private final DefaultRuntimeCamelCatalog catalog;
-    private final List<EipModel> roots = new ArrayList<>();
-    private final Stack<EipModel> models = new Stack<>();
-    private String expression;
-
-    public NewJacksonYamlWriter(Writer writer) throws IOException {
-        this.writer = writer;
-        this.catalog = new DefaultRuntimeCamelCatalog();
-        this.catalog.setCaching(false); // turn cache off as we store state per node
-        this.catalog.setJSonSchemaResolver(new ModelJSonSchemaResolver());
-        this.catalog.start();
-    }
-
-    public void startElement(String name) throws IOException {
-        EipModel model = catalog.eipModel(name);
-        if (model == null) {
-            // not an EIP model
-            return;
-        }
-        EipModel parent = models.isEmpty() ? null : models.peek();
-        model.getMetadata().put("_parent", parent);
-        models.push(model);
-        if (parent == null) {
-            // its a root element
-            roots.add(model);
-        }
-    }
-
-    public void startExpressionElement(String name) throws IOException {
-        // currently building an expression
-        this.expression = name;
-    }
-
-    public void endExpressionElement(String name) throws IOException {
-        // expression complete, back to normal mode
-        this.expression = null;
-    }
-
-    public void endElement(String name) throws IOException {
-        EipModel model = catalog.eipModel(name);
-        if (model == null) {
-            // not an EIP model
-            return;
-        }
-
-        EipModel last = models.isEmpty() ? null : models.peek();
-        if (last != null && isLanguage(last)) {
-            if (!models.isEmpty()) {
-                models.pop();
-            }
-            // okay we ended a language which we need to set on a parent EIP
-            EipModel parent = models.isEmpty() ? null : models.peek();
-            if (parent != null) {
-                String key = expressionName(parent, expression);
-                if (key != null) {
-                    parent.getMetadata().put(key, last);
-                }
-            }
-            return;
-        }
-
-        if (last != null) {
-            if (!models.isEmpty()) {
-                models.pop();
-            }
-            // is this input/output on the parent
-            EipModel parent = models.isEmpty() ? null : models.peek();
-            if (parent != null) {
-                if ("from".equals(name) && parent.isInput()) {
-                    // only set input once
-                    parent.getMetadata().put("_input", last);
-                } else if ("choice".equals(parent.getName())) {
-                    // special for choice/doCatch/doFinally
-                    setMetadata(parent, name, last);
-                } else if (parent.isOutput()) {
-                    List<EipModel> list = (List<EipModel>) parent.getMetadata().get("_output");
-                    if (list == null) {
-                        list = new ArrayList<>();
-                        parent.getMetadata().put("_output", list);
-                    }
-                    list.add(last);
-                }
-            }
-        }
-
-        if (models.isEmpty()) {
-            // we are done
-            writer.write(toYaml());
-        }
-    }
-
-    public void writeText(String name, String text) throws IOException {
-        EipModel last = models.isEmpty() ? null : models.peek();
-        if (last != null) {
-            // special as writeText can be used for list of string values
-            setMetadata(last, name, text);
-        }
-    }
-
-    public void writeValue(String value) throws IOException {
-        EipModel last = models.isEmpty() ? null : models.peek();
-        if (last != null) {
-            String key = valueName(last);
-            if (key != null) {
-                last.getMetadata().put(key, value);
-            }
-        }
-    }
-
-    public void addAttribute(String name, Object value) throws IOException {
-        EipModel last = models.isEmpty() ? null : models.peek();
-        if (last != null) {
-            last.getMetadata().put(name, value);
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    private static void setMetadata(EipModel model, String name, Object value) {
-        // special for choice
-        boolean array = isArray(model, name);
-        if (array) {
-            List list = (List) model.getMetadata().get(name);
-            if (list == null) {
-                list = new ArrayList<>();
-                model.getMetadata().put(name, list);
-            }
-            list.add(value);
-        } else {
-            model.getMetadata().put(name, value);
-        }
-    }
-
-    private static String valueName(EipModel model) {
-        return model.getOptions().stream()
-                .filter(o -> "value".equals(o.getKind()))
-                .map(BaseOptionModel::getName)
-                .findFirst().orElse(null);
-    }
-
-    private static String expressionName(EipModel model, String name) {
-        return model.getOptions().stream()
-                .filter(o -> "expression".equals(o.getKind()))
-                .map(BaseOptionModel::getName)
-                .filter(oName -> name == null || oName.equalsIgnoreCase(name))
-                .findFirst().orElse(null);
-    }
-
-    private static boolean isArray(EipModel model, String name) {
-        return model.getOptions().stream()
-                .filter(o -> o.getName().equalsIgnoreCase(name))
-                .map(o -> "array".equals(o.getType()))
-                .findFirst().orElse(false);
-    }
-
-    private static boolean isLanguage(EipModel model) {
-        return model.getJavaType().startsWith("org.apache.camel.model.language");
-    }
-
-    protected EipNode asExpressionNode(EipModel model, String name) {
-        EipNode node = new EipNode(name, null, false, true);
-        doAsNode(model, node);
-        return node;
-    }
-
-    protected EipNode asNode(EipModel model) {
-        EipNode node = new EipNode(model.getName(), null, false, false);
-        doAsNode(model, node);
-        return node;
-    }
-
-    protected void doAsNode(EipModel model, EipNode node) {
-        for (Map.Entry<String, Object> entry : model.getMetadata().entrySet()) {
-            String key = entry.getKey();
-            if ("_input".equals(key)) {
-                EipModel m = (EipModel) entry.getValue();
-                node.setInput(asNode(m));
-            } else if ("_output".equals(key)) {
-                List<EipModel> list = (List) entry.getValue();
-                for (EipModel m : list) {
-                    node.addOutput(asNode(m));
-                }
-            } else if ("choice".equals(node.getName()) && "otherwise".equals(key)) {
-                EipModel other = (EipModel) entry.getValue();
-                node.addOutput(asNode(other));
-            } else if ("choice".equals(node.getName()) && "when".equals(key)) {
-                Object v = entry.getValue();
-                if (v instanceof List) {
-                    // can be a list in choice
-                    List<EipModel> list = (List) v;
-                    for (EipModel m : list) {
-                        node.addOutput(asNode(m));
-                    }
-                } else {
-                    node.addOutput(asNode((EipModel) v));
-                }
-            } else {
-                boolean skip = key.startsWith("_") || key.equals("customId");
-                if (skip) {
-                    continue;
-                }
-                String exp = null;
-                if (!isLanguage(model)) {
-                    // special for expressions that are a property where we need to use expression name as key
-                    exp = expressionName(model, key);
-                }
-                Object v = entry.getValue();
-                if (v instanceof EipModel) {
-                    EipModel m = (EipModel) entry.getValue();
-                    if (exp == null || "expression".equals(exp)) {
-                        v = asExpressionNode(m, m.getName());
-                    } else {
-                        v = asExpressionNode(m, exp);
-                    }
-                }
-                if (exp != null && v instanceof EipNode) {
-                    node.addExpression((EipNode) v);
-                } else {
-                    node.addProperty(key, v);
-                    if ("expression".equals(key)) {
-                        node.addProperty("language", model.getName());
-                    }
-                }
-            }
-        }
-    }
-
-    public String toYaml() {
-        JsonArray arr = transformToJson(roots);
-
-        // parse JSON
-        try {
-            JsonNode jsonNodeTree = new ObjectMapper().readTree(arr.toJson());
-            // save it as YAML
-            YAMLMapper mapper = new YAMLMapper();
-            mapper.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
-            mapper.enable(YAMLGenerator.Feature.MINIMIZE_QUOTES);
-            mapper.enable(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR);
-            String jsonAsYaml = mapper.writeValueAsString(jsonNodeTree);
-            // strip leading yaml indent of 2 spaces (because INDENT_ARRAYS_WITH_INDICATOR is enabled)
-            StringJoiner sj = new StringJoiner("\n");
-            for (String line : jsonAsYaml.split("\n")) {
-                if (line.startsWith("  ")) {
-                    line = line.substring(2);
-                }
-                sj.add(line);
-            }
-            sj.add(""); // end with empty line
-            jsonAsYaml = sj.toString();
-            return jsonAsYaml;
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
-    protected JsonArray transformToJson(List<EipModel> models) {
-        JsonArray arr = new JsonArray();
-        for (EipModel model : models) {
-            JsonObject jo = asJSonNode(model);
-            arr.add(jo);
-        }
-        return arr;
-    }
-
-    @SuppressWarnings("unchecked")
-    protected JsonObject asJSonNode(EipModel model) {
-        JsonObject answer = new JsonObject();
-        JsonObject jo = new JsonObject();
-        answer.put(model.getName(), jo);
-
-        for (Map.Entry<String, Object> entry : model.getMetadata().entrySet()) {
-            String key = entry.getKey();
-            boolean skip = key.equals("customId");
-            if (skip) {
-                continue;
-            }
-            Object value = entry.getValue();
-            if (value != null) {
-                if (value instanceof Collection<?>) {
-                    Collection<?> col = (Collection<?>) value;
-                    List list = new ArrayList<>();
-                    for (Object v : col) {
-                        Object r = v;
-                        if (r instanceof EipModel) {
-                            EipNode en = asNode((EipModel) r);
-                            value = en.asJsonObject();
-                            JsonObject wrap = new JsonObject();
-                            wrap.put(en.getName(), value);
-                            r = wrap;
-                        }
-                        list.add(r);
-                    }
-                    if ("_output".equals(key)) {
-                        key = "steps";
-                    }
-                    // special with "from" where outputs needs to be embedded
-                    if (jo.containsKey("from")) {
-                        jo = jo.getMap("from");
-                    }
-                    jo.put(key, list);
-                } else {
-                    if (value instanceof EipModel) {
-                        EipNode r = asNode((EipModel) value);
-                        value = r.asJsonObject();
-                        jo.put(r.getName(), value);
-                    } else {
-                        jo.put(key, value);
-                    }
-                }
-            }
-        }
-
-        return answer;
-    }
-
-}
diff --git a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/NewYamlWriter.java b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/NewYamlWriter.java
deleted file mode 100644
index 6d760d5ba37..00000000000
--- a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/NewYamlWriter.java
+++ /dev/null
@@ -1,280 +0,0 @@
-/*
- * 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.yaml.io;
-
-import java.io.IOException;
-import java.io.Writer;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Stack;
-
-import org.apache.camel.catalog.impl.DefaultRuntimeCamelCatalog;
-import org.apache.camel.tooling.model.BaseOptionModel;
-import org.apache.camel.tooling.model.EipModel;
-
-public class NewYamlWriter {
-
-    // doTry / doCatch (special)
-    // circuitBreaker (special)
-
-    private final Writer writer;
-    private final int spaces;
-    private final String lineSeparator;
-    private final DefaultRuntimeCamelCatalog catalog;
-    private final List<EipModel> roots = new ArrayList<>();
-    private final Stack<EipModel> models = new Stack<>();
-    private String expression;
-
-    /**
-     * @param writer not null
-     */
-    public NewYamlWriter(Writer writer) throws IOException {
-        this(writer, 2, null);
-    }
-
-    /**
-     * @param writer        not null
-     * @param spaces        number of spaces to indent
-     * @param lineSeparator could be null, but the normal way is valid line separator ("\n" on UNIX).
-     */
-    public NewYamlWriter(Writer writer, int spaces, String lineSeparator) throws IOException {
-        this.writer = writer;
-        this.spaces = spaces;
-        this.lineSeparator = validateLineSeparator(lineSeparator);
-        this.catalog = new DefaultRuntimeCamelCatalog();
-        this.catalog.setCaching(false); // turn cache off as we store state per node
-        this.catalog.setJSonSchemaResolver(new ModelJSonSchemaResolver());
-        this.catalog.start();
-    }
-
-    private static String validateLineSeparator(String lineSeparator) {
-        String ls = lineSeparator != null ? lineSeparator : System.lineSeparator();
-        if (!(ls.equals("\n") || ls.equals("\r") || ls.equals("\r\n"))) {
-            throw new IllegalArgumentException("Requested line separator is invalid.");
-        }
-        return ls;
-    }
-
-    public void startElement(String name) throws IOException {
-        EipModel model = catalog.eipModel(name);
-        if (model != null) {
-            EipModel parent = models.isEmpty() ? null : models.peek();
-            model.getMetadata().put("_parent", parent);
-            models.push(model);
-            if (parent == null) {
-                // its a root element
-                roots.add(model);
-            }
-        }
-    }
-
-    public void startExpressionElement(String name) throws IOException {
-        // currently building an expression
-        this.expression = name;
-    }
-
-    public void endExpressionElement(String name) throws IOException {
-        // expression complete, back to normal mode
-        this.expression = null;
-    }
-
-    public void endElement(String name) throws IOException {
-        EipModel last = models.isEmpty() ? null : models.peek();
-        if (last != null && isLanguage(last)) {
-            if (!models.isEmpty()) {
-                models.pop();
-            }
-            // okay we ended a language which we need to set on a parent EIP
-            EipModel parent = models.isEmpty() ? null : models.peek();
-            if (parent != null) {
-                String key = expressionName(parent, expression);
-                if (key != null) {
-                    parent.getMetadata().put(key, last);
-                }
-            }
-            return;
-        }
-
-        if (last != null) {
-            if (!models.isEmpty()) {
-                models.pop();
-            }
-            // is this input/output on the parent
-            EipModel parent = models.isEmpty() ? null : models.peek();
-            if (parent != null) {
-                if ("from".equals(name) && parent.isInput()) {
-                    // only set input once
-                    parent.getMetadata().put("_input", last);
-                } else if ("choice".equals(parent.getName())) {
-                    // special for choice/doCatch/doFinally
-                    setMetadata(parent, name, last);
-                } else if (parent.isOutput()) {
-                    List<EipModel> list = (List<EipModel>) parent.getMetadata().get("_output");
-                    if (list == null) {
-                        list = new ArrayList<>();
-                        parent.getMetadata().put("_output", list);
-                    }
-                    list.add(last);
-                }
-            }
-        }
-
-        if (models.isEmpty()) {
-            // we are done
-            writer.write(toYaml());
-        }
-    }
-
-    public void writeText(String name, String text) throws IOException {
-        EipModel last = models.isEmpty() ? null : models.peek();
-        if (last != null) {
-            // special as writeText can be used for list of string values
-            setMetadata(last, name, text);
-        }
-    }
-
-    public void writeValue(String value) throws IOException {
-        EipModel last = models.isEmpty() ? null : models.peek();
-        if (last != null) {
-            String key = valueName(last);
-            if (key != null) {
-                last.getMetadata().put(key, value);
-            }
-        }
-    }
-
-    public void addAttribute(String name, Object value) throws IOException {
-        EipModel last = models.isEmpty() ? null : models.peek();
-        if (last != null) {
-            last.getMetadata().put(name, value);
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    private static void setMetadata(EipModel model, String name, Object value) {
-        // special for choice
-        boolean array = isArray(model, name);
-        if (array) {
-            List list = (List<EipModel>) model.getMetadata().get(name);
-            if (list == null) {
-                list = new ArrayList<>();
-                model.getMetadata().put(name, list);
-            }
-            list.add(value);
-        } else {
-            model.getMetadata().put(name, value);
-        }
-    }
-
-    private static String valueName(EipModel model) {
-        return model.getOptions().stream()
-                .filter(o -> "value".equals(o.getKind()))
-                .map(BaseOptionModel::getName)
-                .findFirst().orElse(null);
-    }
-
-    private static String expressionName(EipModel model, String name) {
-        return model.getOptions().stream()
-                .filter(o -> "expression".equals(o.getKind()))
-                .map(BaseOptionModel::getName)
-                .filter(oName -> name == null || oName.equalsIgnoreCase(name))
-                .findFirst().orElse(null);
-    }
-
-    private static boolean isArray(EipModel model, String name) {
-        return model.getOptions().stream()
-                .filter(o -> o.getName().equalsIgnoreCase(name))
-                .map(o -> "array".equals(o.getType()))
-                .findFirst().orElse(false);
-    }
-
-    private static boolean isLanguage(EipModel model) {
-        return model.getJavaType().startsWith("org.apache.camel.model.language");
-    }
-
-    protected List<EipNode> transformToNodes(List<EipModel> models) {
-        List<EipNode> nodes = new ArrayList<>();
-        for (EipModel model : models) {
-            EipNode node = asNode(model);
-            nodes.add(node);
-        }
-        return nodes;
-    }
-
-    protected EipNode asExpressionNode(EipModel model, String name) {
-        EipNode node = new EipNode(name, null, false, true);
-        doAsNode(model, node);
-        return node;
-    }
-
-    protected EipNode asNode(EipModel model) {
-        EipNode node = new EipNode(model.getName(), null, false, false);
-        doAsNode(model, node);
-        return node;
-    }
-
-    protected void doAsNode(EipModel model, EipNode node) {
-        for (Map.Entry<String, Object> entry : model.getMetadata().entrySet()) {
-            String key = entry.getKey();
-            if ("_input".equals(key)) {
-                EipModel m = (EipModel) entry.getValue();
-                node.setInput(asNode(m));
-            } else if ("_output".equals(key)) {
-                List<EipModel> list = (List) entry.getValue();
-                for (EipModel m : list) {
-                    node.addOutput(asNode(m));
-                }
-            } else {
-                boolean skip = key.startsWith("_") || key.equals("customId");
-                if (skip) {
-                    continue;
-                }
-                String exp = null;
-                if (!isLanguage(model)) {
-                    // special for expressions that are a property where we need to use expression name as key
-                    exp = expressionName(model, key);
-                }
-                Object v = entry.getValue();
-                if (v instanceof EipModel) {
-                    EipModel m = (EipModel) entry.getValue();
-                    if (exp == null || "expression".equals(exp)) {
-                        v = asExpressionNode(m, m.getName());
-                    } else {
-                        v = asExpressionNode(m, exp);
-                    }
-                }
-                if (exp != null && v instanceof EipNode) {
-                    node.addExpression((EipNode) v);
-                } else {
-                    node.addProperty(key, v);
-                }
-            }
-        }
-    }
-
-    public String toYaml() {
-        StringBuilder sb = new StringBuilder();
-        for (EipNode node : transformToNodes(roots)) {
-            String s = node.dump(0, spaces, lineSeparator, true);
-            sb.append(s);
-            sb.append(lineSeparator);
-        }
-        return sb.toString();
-    }
-
-}
diff --git a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/YamlWriter.java b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/YamlWriter.java
index 2221d6774bc..c7c6e331134 100644
--- a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/YamlWriter.java
+++ b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/io/YamlWriter.java
@@ -18,184 +18,371 @@ package org.apache.camel.yaml.io;
 
 import java.io.IOException;
 import java.io.Writer;
-import java.util.ArrayDeque;
 import java.util.ArrayList;
-import java.util.Deque;
+import java.util.Collection;
 import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+import java.util.StringJoiner;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import org.apache.camel.catalog.impl.DefaultRuntimeCamelCatalog;
+import org.apache.camel.tooling.model.BaseOptionModel;
+import org.apache.camel.tooling.model.EipModel;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
 
 /**
- * YAML writer which emits nicely formatted documents.
+ * YAML writer which uses Jackson to dump to yaml format.
+ *
+ * Implementation notes:
+ *
+ * This writer is based on the same principle for the XML writer which parses the Camel routes (model classes) and emit
+ * a StAX based events for start/end elements.
+ *
+ * However since the YAML DSL is not as easy to dump as XML, then we need to enrich with additional metadata from the
+ * runtime catalog ({@link EipModel}). We then abuse the {@link EipModel} and store the route details in its metadata.
+ * After this we transform from {@link EipModel} to {@link EipNode} to have a List/Map structure that we then transform
+ * to JSon, and then from JSon to YAML.
+ *
  */
 public class YamlWriter {
 
     private final Writer writer;
-    private final int spaces;
-    private final String lineSeparator;
+    private final DefaultRuntimeCamelCatalog catalog;
+    private final List<EipModel> roots = new ArrayList<>();
+    private boolean routesIsRoot;
+    private final Stack<EipModel> models = new Stack<>();
+    private String expression;
 
-    private final List<EipNode> root = new ArrayList<>();
-    private final Deque<EipNode> stack = new ArrayDeque<>();
-
-    /**
-     * @param writer not null
-     */
     public YamlWriter(Writer writer) throws IOException {
-        this(writer, 2, null);
+        this.writer = writer;
+        this.catalog = new DefaultRuntimeCamelCatalog();
+        this.catalog.setCaching(false); // turn cache off as we store state per node
+        this.catalog.setJSonSchemaResolver(new ModelJSonSchemaResolver());
+        this.catalog.start();
     }
 
-    /**
-     * @param writer        not null
-     * @param spaces        number of spaces to indent
-     * @param lineSeparator could be null, but the normal way is valid line separator ("\n" on UNIX).
-     */
-    public YamlWriter(Writer writer, int spaces, String lineSeparator) throws IOException {
-        this.writer = writer;
-        this.spaces = spaces;
-        this.lineSeparator = validateLineSeparator(lineSeparator);
+    public void startElement(String name) throws IOException {
+        if ("routes".equals(name)) {
+            routesIsRoot = true;
+            return;
+        }
+
+        EipModel model = catalog.eipModel(name);
+        if (model == null) {
+            // not an EIP model
+            return;
+        }
+
+        EipModel parent = models.isEmpty() ? null : models.peek();
+        model.getMetadata().put("_parent", parent);
+        models.push(model);
+        if (parent == null) {
+            // its a root element
+            roots.add(model);
+        }
+    }
+
+    public void startExpressionElement(String name) throws IOException {
+        // currently building an expression
+        this.expression = name;
+    }
+
+    public void endExpressionElement(String name) throws IOException {
+        // expression complete, back to normal mode
+        this.expression = null;
+    }
+
+    public void endElement(String name) throws IOException {
+        if ("routes".equals(name)) {
+            // we are done
+            writer.write(toYaml());
+            return;
+        }
+
+        EipModel model = catalog.eipModel(name);
+        if (model == null) {
+            // not an EIP model
+            return;
+        }
+
+        EipModel last = models.isEmpty() ? null : models.peek();
+        if (last != null && isLanguage(last)) {
+            if (!models.isEmpty()) {
+                models.pop();
+            }
+            // okay we ended a language which we need to set on a parent EIP
+            EipModel parent = models.isEmpty() ? null : models.peek();
+            if (parent != null) {
+                String key = expressionName(parent, expression);
+                if (key != null) {
+                    parent.getMetadata().put(key, last);
+                }
+            }
+            return;
+        }
+
+        if (last != null) {
+            if (!models.isEmpty()) {
+                models.pop();
+            }
+            // is this input/output on the parent
+            EipModel parent = models.isEmpty() ? null : models.peek();
+            if (parent != null) {
+                if ("from".equals(name) && parent.isInput()) {
+                    // only set input once
+                    parent.getMetadata().put("_input", last);
+                } else if ("choice".equals(parent.getName())) {
+                    // special for choice/doCatch/doFinally
+                    setMetadata(parent, name, last);
+                } else if (parent.isOutput()) {
+                    List<EipModel> list = (List<EipModel>) parent.getMetadata().get("_output");
+                    if (list == null) {
+                        list = new ArrayList<>();
+                        parent.getMetadata().put("_output", list);
+                    }
+                    list.add(last);
+                } else if ("marshal".equals(parent.getName()) || "unmarshal".equals(parent.getName())) {
+                    parent.getMetadata().put("_dataFormatType", last);
+                }
+            }
+        }
+
+        if (models.isEmpty() && !routesIsRoot) {
+            // we are done
+            writer.write(toYaml());
+        }
     }
 
-    private static String validateLineSeparator(String lineSeparator) {
-        String ls = lineSeparator != null ? lineSeparator : System.lineSeparator();
-        if (!(ls.equals("\n") || ls.equals("\r") || ls.equals("\r\n"))) {
-            throw new IllegalArgumentException("Requested line separator is invalid.");
+    public void writeText(String name, String text) throws IOException {
+        EipModel last = models.isEmpty() ? null : models.peek();
+        if (last != null) {
+            // special as writeText can be used for list of string values
+            setMetadata(last, name, text);
         }
-        return ls;
     }
 
-    private EipNode getParent() {
-        for (EipNode node : stack) {
-            if (node.isOutput()) {
-                return node;
+    public void writeValue(String value) throws IOException {
+        EipModel last = models.isEmpty() ? null : models.peek();
+        if (last != null) {
+            String key = valueName(last);
+            if (key != null) {
+                last.getMetadata().put(key, value);
             }
         }
-        return null;
     }
 
-    public void startElement(String name)
-            throws IOException {
+    public void addAttribute(String name, Object value) throws IOException {
+        EipModel last = models.isEmpty() ? null : models.peek();
+        if (last != null) {
+            last.getMetadata().put(name, value);
+        }
+    }
 
-        //    public void startElement(String name, boolean supportOutput, boolean supportExpression, boolean language)
-        //            throws IOException {
-        //        EipNode parent = getParent();
-        //        EipNode node = new EipNode(name, parent, supportOutput, supportExpression);
+    private EipNode asExpressionNode(EipModel model, String name) {
+        EipNode node = new EipNode(name, null, false, true);
+        doAsNode(model, node);
+        return node;
+    }
 
-        /*
-        if (parent != null && "from".equals(name)) {
-            parent.setInput(node);
-        } else {
-            EipNode last = !stack.isEmpty() ? stack.peek() : null;
-            if (last != null && language) {
-                last.addExpression(node);
-            } else if (last != null && last.isOutput()) {
-                last.addOutput(node);
+    private EipNode asNode(EipModel model) {
+        EipNode node = new EipNode(model.getName(), null, false, false);
+        doAsNode(model, node);
+        return node;
+    }
+
+    private void doAsNode(EipModel model, EipNode node) {
+        for (Map.Entry<String, Object> entry : model.getMetadata().entrySet()) {
+            String key = entry.getKey();
+            if ("_input".equals(key)) {
+                EipModel m = (EipModel) entry.getValue();
+                node.setInput(asNode(m));
+            } else if ("_output".equals(key)) {
+                List<EipModel> list = (List) entry.getValue();
+                for (EipModel m : list) {
+                    node.addOutput(asNode(m));
+                }
+            } else if ("choice".equals(node.getName()) && "otherwise".equals(key)) {
+                EipModel other = (EipModel) entry.getValue();
+                node.addOutput(asNode(other));
+            } else if ("choice".equals(node.getName()) && "when".equals(key)) {
+                Object v = entry.getValue();
+                if (v instanceof List) {
+                    // can be a list in choice
+                    List<EipModel> list = (List) v;
+                    for (EipModel m : list) {
+                        node.addOutput(asNode(m));
+                    }
+                } else {
+                    node.addOutput(asNode((EipModel) v));
+                }
+            } else if (("marshal".equals(node.getName()) || "unmarshal".equals(node.getName()))
+                    && "_dataFormatType".equals(key)) {
+                EipModel other = (EipModel) entry.getValue();
+                node.addOutput(asNode(other));
             } else {
-                boolean added = addOutput(node, parent);
-                if (!added) {
-                    root.add(node);
+                boolean skip = key.startsWith("_") || key.equals("customId");
+                if (skip) {
+                    continue;
+                }
+                String exp = null;
+                if (!isLanguage(model)) {
+                    // special for expressions that are a property where we need to use expression name as key
+                    exp = expressionName(model, key);
+                }
+                Object v = entry.getValue();
+                if (v instanceof EipModel) {
+                    EipModel m = (EipModel) entry.getValue();
+                    if (exp == null || "expression".equals(exp)) {
+                        v = asExpressionNode(m, m.getName());
+                    } else {
+                        v = asExpressionNode(m, exp);
+                    }
+                }
+                if (exp != null && v instanceof EipNode) {
+                    node.addExpression((EipNode) v);
+                } else {
+                    node.addProperty(key, v);
+                    if ("expression".equals(key)) {
+                        node.addProperty("language", model.getName());
+                    }
                 }
             }
-            //        } else if (language) {
-            //            addExpression(node, parent);
-            //        } else {
-            //            boolean added = addOutput(node, parent);
-            //            if (!added) {
-            //                root.add(node);
-            //            }
-        }
-        stack.push(node);
-        */
-    }
-
-    private void addExpression(EipNode node, EipNode parent) {
-        EipNode last = !stack.isEmpty() ? stack.peek() : null;
-        if (last != null && last.isExpression()) {
-            last.addExpression(node);
-        } else if (parent != null) {
-            parent.addExpression(node);
-            //            if (parent.isExpression()) {
-            //                parent.addExpression(node);
-            //            } else if (parent.isOutput()) {
-            //                parent.addOutput(node);
-            //            } else {
-            //                addExpression(node, parent.getParent());
-            //            }
-        }
-    }
-
-    private boolean addOutput(EipNode node, EipNode parent) {
-        if (parent == null) {
-            return false;
-        }
-        if (parent.isOutput()) {
-            parent.addOutput(node);
-            return true;
-        } else {
-            return addOutput(node, parent.getParent());
         }
     }
 
-    public void writeText(String text) throws IOException {
-        EipNode node = stack.peek();
-        if (node != null) {
-            node.setText(text);
+    public String toYaml() {
+        JsonArray arr = transformToJson(roots);
+
+        // parse JSON
+        try {
+            JsonNode jsonNodeTree = new ObjectMapper().readTree(arr.toJson());
+            // save it as YAML
+            YAMLMapper mapper = new YAMLMapper();
+            mapper.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
+            mapper.enable(YAMLGenerator.Feature.MINIMIZE_QUOTES);
+            mapper.enable(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR);
+            String jsonAsYaml = mapper.writeValueAsString(jsonNodeTree);
+            // strip leading yaml indent of 2 spaces (because INDENT_ARRAYS_WITH_INDICATOR is enabled)
+            StringJoiner sj = new StringJoiner("\n");
+            for (String line : jsonAsYaml.split("\n")) {
+                if (line.startsWith("  ")) {
+                    line = line.substring(2);
+                }
+                sj.add(line);
+            }
+            sj.add(""); // end with empty line
+            jsonAsYaml = sj.toString();
+            return jsonAsYaml;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
         }
     }
 
-    public void addAttribute(String key, Object value) throws IOException {
-        // skip unwanted IDs
-        if ("customId".equals(key)) {
-            return;
+    private JsonArray transformToJson(List<EipModel> models) {
+        JsonArray arr = new JsonArray();
+        for (EipModel model : models) {
+            JsonObject jo = asJSonNode(model);
+            arr.add(jo);
         }
+        return arr;
+    }
+
+    @SuppressWarnings("unchecked")
+    private JsonObject asJSonNode(EipModel model) {
+        JsonObject answer = new JsonObject();
+        JsonObject jo = new JsonObject();
+        answer.put(model.getName(), jo);
 
-        EipNode node = stack.peek();
-        if (node != null) {
-            node.addProperty(key, value);
+        for (Map.Entry<String, Object> entry : model.getMetadata().entrySet()) {
+            String key = entry.getKey();
+            boolean skip = key.equals("customId");
+            if (skip) {
+                continue;
+            }
+            Object value = entry.getValue();
+            if (value != null) {
+                if (value instanceof Collection<?>) {
+                    Collection<?> col = (Collection<?>) value;
+                    List list = new ArrayList<>();
+                    for (Object v : col) {
+                        Object r = v;
+                        if (r instanceof EipModel) {
+                            EipNode en = asNode((EipModel) r);
+                            value = en.asJsonObject();
+                            JsonObject wrap = new JsonObject();
+                            wrap.put(en.getName(), value);
+                            r = wrap;
+                        }
+                        list.add(r);
+                    }
+                    if ("_output".equals(key)) {
+                        key = "steps";
+                    }
+                    // special with "from" where outputs needs to be embedded
+                    if (jo.containsKey("from")) {
+                        jo = jo.getMap("from");
+                    }
+                    jo.put(key, list);
+                } else {
+                    if (value instanceof EipModel) {
+                        EipNode r = asNode((EipModel) value);
+                        value = r.asJsonObject();
+                        jo.put(r.getName(), value);
+                    } else {
+                        jo.put(key, value);
+                    }
+                }
+            }
         }
+
+        return answer;
     }
 
-    public void endElement() throws IOException {
-        if (!stack.isEmpty()) {
-            stack.pop();
-        }
-        if (stack.isEmpty()) {
-            // we are done
-            writer.write(toYaml());
+    @SuppressWarnings("unchecked")
+    private static void setMetadata(EipModel model, String name, Object value) {
+        // special for choice
+        boolean array = isArray(model, name);
+        if (array) {
+            List list = (List) model.getMetadata().get(name);
+            if (list == null) {
+                list = new ArrayList<>();
+                model.getMetadata().put(name, list);
+            }
+            list.add(value);
+        } else {
+            model.getMetadata().put(name, value);
         }
     }
 
-    /**
-     * Write a string to the underlying writer
-     */
-    private void write(String str) throws IOException {
-        getWriter().write(str);
+    private static String valueName(EipModel model) {
+        return model.getOptions().stream()
+                .filter(o -> "value".equals(o.getKind()))
+                .map(BaseOptionModel::getName)
+                .findFirst().orElse(null);
     }
 
-    /**
-     * Get the string used as line separator or LS if not set.
-     *
-     * @return the line separator
-     * @see    System#lineSeparator()
-     */
-    protected String getLineSeparator() {
-        return lineSeparator;
+    private static String expressionName(EipModel model, String name) {
+        return model.getOptions().stream()
+                .filter(o -> "expression".equals(o.getKind()))
+                .map(BaseOptionModel::getName)
+                .filter(oName -> name == null || oName.equalsIgnoreCase(name))
+                .findFirst().orElse(null);
     }
 
-    /**
-     * Get the underlying writer
-     *
-     * @return the underlying writer
-     */
-    protected Writer getWriter() {
-        return writer;
+    private static boolean isArray(EipModel model, String name) {
+        return model.getOptions().stream()
+                .filter(o -> o.getName().equalsIgnoreCase(name))
+                .map(o -> "array".equals(o.getType()))
+                .findFirst().orElse(false);
     }
 
-    public String toYaml() {
-        StringBuilder sb = new StringBuilder();
-        for (EipNode node : root) {
-            String s = node.dump(0, spaces, lineSeparator, true);
-            sb.append(s);
-            sb.append(lineSeparator);
-        }
-        return sb.toString();
+    private static boolean isLanguage(EipModel model) {
+        return model.getJavaType().startsWith("org.apache.camel.model.language");
     }
 
 }
diff --git a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/BaseWriter.java b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/BaseWriter.java
index 876c600c200..4b751c4bc59 100644
--- a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/BaseWriter.java
+++ b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/BaseWriter.java
@@ -22,14 +22,14 @@ import java.util.List;
 
 import org.w3c.dom.Element;
 
-import org.apache.camel.yaml.io.NewJacksonYamlWriter;
+import org.apache.camel.yaml.io.YamlWriter;
 
 public class BaseWriter {
 
-    protected final NewJacksonYamlWriter writer;
+    protected final YamlWriter writer;
 
     public BaseWriter(Writer writer, String namespace) throws IOException {
-        this.writer = new NewJacksonYamlWriter(writer);
+        this.writer = new YamlWriter(writer);
         // namespace is only for XML
     }
 
diff --git a/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/ModelWriterTest.java b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/ModelWriterTest.java
index e578168a523..12f21a58386 100644
--- a/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/ModelWriterTest.java
+++ b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/ModelWriterTest.java
@@ -28,11 +28,14 @@ import org.apache.camel.model.ChoiceDefinition;
 import org.apache.camel.model.ExpressionSubElementDefinition;
 import org.apache.camel.model.FromDefinition;
 import org.apache.camel.model.LogDefinition;
+import org.apache.camel.model.MarshalDefinition;
 import org.apache.camel.model.ModelCamelContext;
 import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.RoutesDefinition;
 import org.apache.camel.model.SetBodyDefinition;
 import org.apache.camel.model.SplitDefinition;
 import org.apache.camel.model.ToDefinition;
+import org.apache.camel.model.dataformat.CsvDataFormat;
 import org.apache.camel.model.language.ConstantExpression;
 import org.apache.camel.model.language.HeaderExpression;
 import org.apache.camel.model.language.SimpleExpression;
@@ -240,4 +243,57 @@ public class ModelWriterTest {
         Assertions.assertEquals(expected, out);
     }
 
+    @Test
+    public void testTwoRoutes() throws Exception {
+        StringWriter sw = new StringWriter();
+        ModelWriter writer = new ModelWriter(sw);
+
+        RoutesDefinition routes = new RoutesDefinition();
+
+        RouteDefinition route = new RouteDefinition();
+        route.setId("myRoute0");
+        route.setInput(new FromDefinition("timer:yaml?period=1000"));
+        SetBodyDefinition sb = new SetBodyDefinition();
+        sb.setExpression(new ConstantExpression("Hello from yaml"));
+        route.addOutput(sb);
+        route.addOutput(new LogDefinition("${body}"));
+        routes.getRoutes().add(route);
+
+        route = new RouteDefinition();
+        route.setId("myRoute1");
+        route.setInput(new FromDefinition("direct:start"));
+        ToDefinition to = new ToDefinition("log:input");
+        route.addOutput(to);
+        ToDefinition to2 = new ToDefinition("mock:result");
+        to2.setPattern("InOut");
+        route.addOutput(to2);
+        routes.getRoutes().add(route);
+
+        writer.writeRoutesDefinition(routes);
+
+        String out = sw.toString();
+        String expected = IOHelper.loadText(new FileInputStream("src/test/resources/route8.yaml"));
+        Assertions.assertEquals(expected, out);
+    }
+
+    @Test
+    public void testMarshal() throws Exception {
+        StringWriter sw = new StringWriter();
+        ModelWriter writer = new ModelWriter(sw);
+
+        RouteDefinition route = new RouteDefinition();
+        route.setId("myRoute9");
+        route.setInput(new FromDefinition("timer:foo"));
+        MarshalDefinition mar = new MarshalDefinition();
+        mar.setDataFormatType(new CsvDataFormat());
+        route.addOutput(mar);
+        route.addOutput(new LogDefinition("${body}"));
+
+        writer.writeRouteDefinition(route);
+
+        String out = sw.toString();
+        String expected = IOHelper.loadText(new FileInputStream("src/test/resources/route9.yaml"));
+        Assertions.assertEquals(expected, out);
+    }
+
 }
diff --git a/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/XmlToYamlTest.java b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/XmlToYamlTest.java
new file mode 100644
index 00000000000..484fa4e16fb
--- /dev/null
+++ b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/XmlToYamlTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.yaml.out;
+
+import java.io.FileInputStream;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.stream.Stream;
+
+import org.apache.camel.model.RoutesDefinition;
+import org.apache.camel.xml.in.ModelParser;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class XmlToYamlTest {
+
+    public static final String NAMESPACE = "http://camel.apache.org/schema/spring";
+
+    private static final Logger LOG = LoggerFactory.getLogger(XmlToYamlTest.class);
+
+    @ParameterizedTest
+    @MethodSource("routes")
+    @DisplayName("Test xml to yaml for <routes>")
+    void testRoutes(String xml) throws Exception {
+        try (InputStream is = new FileInputStream("../camel-xml-io/src/test/resources/" + xml)) {
+            RoutesDefinition expected = new ModelParser(is, NAMESPACE).parseRoutesDefinition().get();
+            StringWriter sw = new StringWriter();
+            new ModelWriter(sw).writeRoutesDefinition(expected);
+            LOG.info("xml={}\n{}\n", xml, sw);
+        }
+    }
+
+    private static Stream<Arguments> routes() {
+        return definitions("routes");
+    }
+
+    private static Stream<Arguments> definitions(String xml) {
+        try {
+            return Files.list(Paths.get("../camel-xml-io/src/test/resources"))
+                    .filter(p -> {
+                        try {
+                            return Files.isRegularFile(p)
+                                    && p.getFileName().toString().endsWith(".xml")
+                                    && Files.readString(p).contains("<" + xml);
+                        } catch (IOException e) {
+                            throw new IOError(e);
+                        }
+                    })
+                    .map(p -> p.getFileName().toString())
+                    .flatMap(p -> Stream.of(Arguments.of(p, NAMESPACE)));
+        } catch (IOException e) {
+            throw new IOError(e);
+        }
+    }
+
+}
diff --git a/core/camel-yaml-io/src/test/resources/route8.yaml b/core/camel-yaml-io/src/test/resources/route8.yaml
new file mode 100644
index 00000000000..cc50bc143c9
--- /dev/null
+++ b/core/camel-yaml-io/src/test/resources/route8.yaml
@@ -0,0 +1,20 @@
+- route:
+    id: myRoute0
+    from:
+      uri: timer:yaml?period=1000
+      steps:
+        - setBody:
+            constant:
+              expression: Hello from yaml
+        - log:
+            message: "${body}"
+- route:
+    id: myRoute1
+    from:
+      uri: direct:start
+      steps:
+        - to:
+            uri: log:input
+        - to:
+            uri: mock:result
+            pattern: InOut
diff --git a/core/camel-yaml-io/src/test/resources/route9.yaml b/core/camel-yaml-io/src/test/resources/route9.yaml
new file mode 100644
index 00000000000..1f02c067d7a
--- /dev/null
+++ b/core/camel-yaml-io/src/test/resources/route9.yaml
@@ -0,0 +1,9 @@
+- route:
+    id: myRoute9
+    from:
+      uri: timer:foo
+      steps:
+        - marshal:
+            csv: {}
+        - log:
+            message: "${body}"