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

shuber pushed a commit to branch unomi-3-dev
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/unomi-3-dev by this push:
     new a779bf7f7 UNOMI-920 Add safe string conversion utility with length 
checks in `DefaultRequestTracer`, integrate YAML serialization capabilities 
across core entities, and test circular reference detection.
a779bf7f7 is described below

commit a779bf7f7f42c61638e81d9f0cd4f32ba9aea6b0
Author: Serge Huber <[email protected]>
AuthorDate: Thu Jan 1 16:20:40 2026 +0100

    UNOMI-920 Add safe string conversion utility with length checks in 
`DefaultRequestTracer`, integrate YAML serialization capabilities across core 
entities, and test circular reference detection.
---
 api/pom.xml                                        |   4 +
 api/src/main/java/org/apache/unomi/api/Item.java   |  68 +++-
 .../main/java/org/apache/unomi/api/Metadata.java   |  56 +++-
 .../java/org/apache/unomi/api/MetadataItem.java    |  50 ++-
 .../main/java/org/apache/unomi/api/Parameter.java  |  40 ++-
 .../java/org/apache/unomi/api/actions/Action.java  |  48 ++-
 .../org/apache/unomi/api/actions/ActionType.java   |  42 ++-
 .../org/apache/unomi/api/conditions/Condition.java |  96 ++++--
 .../apache/unomi/api/conditions/ConditionType.java |  50 ++-
 .../unomi/api/conditions/ConditionValidation.java  |  10 +-
 .../main/java/org/apache/unomi/api/goals/Goal.java |  44 ++-
 .../main/java/org/apache/unomi/api/rules/Rule.java |  46 ++-
 .../org/apache/unomi/api/segments/Scoring.java     |  38 ++-
 .../apache/unomi/api/segments/ScoringElement.java  |  47 ++-
 .../org/apache/unomi/api/segments/Segment.java     |  40 ++-
 .../java/org/apache/unomi/api/utils/YamlUtils.java |  94 +++++-
 .../org/apache/unomi/api/utils/YamlUtilsTest.java  | 357 ++++++++++++++++++++-
 .../unomi/tracing/impl/DefaultRequestTracer.java   |  29 +-
 .../tracing/impl/DefaultTracerServiceTest.java     | 125 +++++---
 19 files changed, 1125 insertions(+), 159 deletions(-)

diff --git a/api/pom.xml b/api/pom.xml
index 5b0072b5f..63f48d7b4 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -88,6 +88,10 @@
             <artifactId>osgi.core</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.yaml</groupId>
+            <artifactId>snakeyaml</artifactId>
+        </dependency>
 
         <!-- Test Dependencies -->
         <dependency>
diff --git a/api/src/main/java/org/apache/unomi/api/Item.java 
b/api/src/main/java/org/apache/unomi/api/Item.java
index 3e591fb74..44da45f5c 100644
--- a/api/src/main/java/org/apache/unomi/api/Item.java
+++ b/api/src/main/java/org/apache/unomi/api/Item.java
@@ -17,15 +17,18 @@
 
 package org.apache.unomi.api;
 
+import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.Serializable;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
+
 /**
  * A context server tracked entity. All tracked entities need to extend this 
class so as to provide the minimal information the context server needs to be 
able to track such
  * entities and operate on them. Items are persisted according to their type 
(structure) and identifier (identity). Of note, all Item subclasses 
<strong>must</strong> define a
@@ -37,7 +40,7 @@ import java.util.concurrent.ConcurrentHashMap;
  * though scopes could span across sites depending on the desired analysis 
granularity). Scopes allow clients accessing the context server to filter data. 
The context server
  * defines a built-in scope ({@link Metadata#SYSTEM_SCOPE}) that clients can 
use to share data across scopes.
  */
-public abstract class Item implements Serializable {
+public abstract class Item implements Serializable, YamlConvertible {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(Item.class.getName());
 
     private static final long serialVersionUID = 1217180125083162915L;
@@ -222,4 +225,61 @@ public abstract class Item implements Serializable {
     public void setLastSyncDate(Date lastSyncDate) {
         this.lastSyncDate = lastSyncDate;
     }
+
+    /**
+     * Converts this item to a Map structure for YAML output.
+     * Implements YamlConvertible interface with circular reference detection.
+     *
+     * @param visited set of already visited objects to prevent infinite 
recursion (may be null)
+     * @return a Map representation of this item
+     */
+    @Override
+    public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+        if (maxDepth <= 0) {
+            return YamlMapBuilder.create()
+                .put("itemId", itemId)
+                .put("itemType", itemType)
+                .put("systemMetadata", "<max depth exceeded>")
+                .build();
+        }
+        final Set<Object> visitedSet = visited != null ? visited : new 
HashSet<>();
+        // Check if already visited - if so, we're being called from a child 
class via super.toYaml()
+        // OR it's a real circular reference. We can't distinguish, but since 
child classes
+        // (like Rule, ConditionType, etc.) all check for circular refs before 
calling super,
+        // if we're already visited here, it's safe to assume it's a super 
call, not a circular ref.
+        // If Item is directly serialized and encounters itself, the check 
would happen at the
+        // top level before nested processing, so this should be safe.
+        boolean alreadyVisited = visitedSet.contains(this);
+        if (!alreadyVisited) {
+            // First time seeing this object - add it to track for circular 
references
+            visitedSet.add(this);
+        }
+        try {
+            return YamlMapBuilder.create()
+                .put("itemId", itemId)  // Always include, even if null, to 
reflect actual state
+                .put("itemType", itemType)  // Always include, even if null, 
to reflect actual state
+                .putIfNotNull("scope", scope)
+                .putIfNotNull("version", version)
+                .putIfNotNull("systemMetadata", systemMetadata != null && 
!systemMetadata.isEmpty() ? toYamlValue(systemMetadata, visitedSet, maxDepth - 
1) : null)
+                .putIfNotNull("tenantId", tenantId)
+                .putIfNotNull("createdBy", createdBy)
+                .putIfNotNull("lastModifiedBy", lastModifiedBy)
+                .putIfNotNull("creationDate", creationDate)
+                .putIfNotNull("lastModificationDate", lastModificationDate)
+                .putIfNotNull("sourceInstanceId", sourceInstanceId)
+                .putIfNotNull("lastSyncDate", lastSyncDate)
+                .build();
+        } finally {
+            // Only remove if we added it (i.e., if it wasn't already visited)
+            if (!alreadyVisited) {
+                visitedSet.remove(this);
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        Map<String, Object> map = toYaml();
+        return YamlUtils.format(map);
+    }
 }
diff --git a/api/src/main/java/org/apache/unomi/api/Metadata.java 
b/api/src/main/java/org/apache/unomi/api/Metadata.java
index 7e0f8da8d..ec25c7fa8 100644
--- a/api/src/main/java/org/apache/unomi/api/Metadata.java
+++ b/api/src/main/java/org/apache/unomi/api/Metadata.java
@@ -17,16 +17,24 @@
 
 package org.apache.unomi.api;
 
+import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
+
 import java.io.Serializable;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
+import java.util.Map;
 import java.util.Set;
 
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+
 /**
  * A class providing information about context server entities.
  *
  * @see MetadataItem
  */
-public class Metadata implements Comparable<Metadata>, Serializable {
+public class Metadata implements Comparable<Metadata>, Serializable, 
YamlConvertible {
 
     private static final long serialVersionUID = 7446061538573517071L;
 
@@ -279,19 +287,41 @@ public class Metadata implements Comparable<Metadata>, 
Serializable {
         return result;
     }
 
+    /**
+     * Converts this metadata to a Map structure for YAML output.
+     * Implements YamlConvertible interface with circular reference detection.
+     *
+     * @param visited set of already visited objects to prevent infinite 
recursion (may be null)
+     * @return a Map representation of this metadata
+     */
+    @Override
+    public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+        if (visited != null && visited.contains(this)) {
+            return circularRef();
+        }
+        final Set<Object> visitedSet = visited != null ? visited : new 
HashSet<>();
+        visitedSet.add(this);
+        try {
+            return YamlMapBuilder.create()
+                .putIfNotNull("id", id)
+                .putIfNotNull("name", name)
+                .putIfNotNull("description", description)
+                .putIfNotNull("scope", scope)
+                .putIfNotEmpty("tags", tags)
+                .putIfNotEmpty("systemTags", systemTags)
+                .putIf("enabled", true, enabled)
+                .putIf("missingPlugins", true, missingPlugins)
+                .putIf("hidden", true, hidden)
+                .putIf("readOnly", true, readOnly)
+                .build();
+        } finally {
+            visitedSet.remove(this);
+        }
+    }
+
     @Override
     public String toString() {
-        return "Metadata{" +
-                "id='" + id + '\'' +
-                ", name='" + name + '\'' +
-                ", description='" + description + '\'' +
-                ", scope='" + scope + '\'' +
-                ", tags=" + tags +
-                ", systemTags=" + systemTags +
-                ", enabled=" + enabled +
-                ", missingPlugins=" + missingPlugins +
-                ", hidden=" + hidden +
-                ", readOnly=" + readOnly +
-                '}';
+        Map<String, Object> map = toYaml();
+        return YamlUtils.format(map);
     }
 }
diff --git a/api/src/main/java/org/apache/unomi/api/MetadataItem.java 
b/api/src/main/java/org/apache/unomi/api/MetadataItem.java
index 6a23ea9d9..35ef596a2 100644
--- a/api/src/main/java/org/apache/unomi/api/MetadataItem.java
+++ b/api/src/main/java/org/apache/unomi/api/MetadataItem.java
@@ -17,8 +17,16 @@
 
 package org.apache.unomi.api;
 
+import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
+
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlTransient;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
 
 /**
  * A superclass for all {@link Item}s that bear {@link Metadata}.
@@ -60,10 +68,46 @@ public abstract class MetadataItem extends Item {
         return scope;
     }
 
+    /**
+     * Converts this metadata item to a Map structure for YAML output.
+     * Merges fields from Item parent class and adds metadata field.
+     * Subclasses should override this method, call super.toYaml(visited), and 
add their specific fields.
+     *
+     * @param visited set of already visited objects to prevent infinite 
recursion (may be null)
+     * @return a Map representation of this metadata item
+     */
+    @Override
+    public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+        if (maxDepth <= 0) {
+            return YamlMapBuilder.create()
+                .put("metadata", "<max depth exceeded>")
+                .build();
+        }
+        final Set<Object> visitedSet = visited != null ? visited : new 
HashSet<>();
+        // Check if already visited - if so, we're being called from a child 
class via super.toYaml()
+        // In that case, skip the circular reference check and just proceed
+        boolean alreadyVisited = visitedSet.contains(this);
+        if (!alreadyVisited) {
+            // Only check for circular references if this is the first time 
we're seeing this object
+            visitedSet.add(this);
+        }
+        try {
+            return YamlMapBuilder.create()
+                .mergeObject(super.toYaml(visitedSet, maxDepth))
+                .putIfNotNull("metadata", metadata != null ? 
toYamlValue(metadata, visitedSet, maxDepth - 1) : null)
+                .build();
+        } finally {
+            // Only remove if we added it (i.e., if it wasn't already visited)
+            if (!alreadyVisited) {
+                visitedSet.remove(this);
+            }
+        }
+    }
+
+
     @Override
     public String toString() {
-        return "MetadataItem{" +
-                "metadata=" + metadata +
-                '}';
+        Map<String, Object> map = toYaml();
+        return YamlUtils.format(map);
     }
 }
diff --git a/api/src/main/java/org/apache/unomi/api/Parameter.java 
b/api/src/main/java/org/apache/unomi/api/Parameter.java
index 7155472f7..0e24a2e21 100644
--- a/api/src/main/java/org/apache/unomi/api/Parameter.java
+++ b/api/src/main/java/org/apache/unomi/api/Parameter.java
@@ -18,14 +18,20 @@
 package org.apache.unomi.api;
 
 import org.apache.unomi.api.conditions.ConditionValidation;
+import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
 
 import java.io.Serializable;
+import java.util.Map;
+import java.util.Set;
+
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
 
 /**
  * A representation of a condition parameter, to be used in the segment 
building UI to either select parameters from a
  * choicelist or to enter a specific value.
  */
-public class Parameter implements Serializable {
+public class Parameter implements Serializable, YamlConvertible {
 
     private static final long serialVersionUID = 6019392686888941547L;
 
@@ -93,14 +99,32 @@ public class Parameter implements Serializable {
         this.validation = validation;
     }
 
+    /**
+     * Converts this parameter to a Map structure for YAML output.
+     * Implements YamlConvertible interface.
+     *
+     * @param visited set of already visited objects to prevent infinite 
recursion (may be null)
+     * @return a Map representation of this parameter
+     */
+    @Override
+    public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+        if (maxDepth <= 0) {
+            return YamlUtils.YamlMapBuilder.create()
+                .put("id", id)
+                .put("validation", "<max depth exceeded>")
+                .build();
+        }
+        return YamlUtils.YamlMapBuilder.create()
+            .putIfNotNull("id", id)
+            .putIfNotNull("type", type)
+            .putIf("multivalued", true, multivalued)
+            .putIfNotNull("defaultValue", defaultValue)
+            .putIfNotNull("validation", validation != null ? 
toYamlValue(validation, visited, maxDepth - 1) : null)
+            .build();
+    }
+
     @Override
     public String toString() {
-        return "Parameter{" +
-                "id='" + id + '\'' +
-                ", type='" + type + '\'' +
-                ", multivalued=" + multivalued +
-                ", defaultValue=" + defaultValue +
-                ", validation=" + validation +
-                '}';
+        return YamlUtils.format(toYaml());
     }
 }
diff --git a/api/src/main/java/org/apache/unomi/api/actions/Action.java 
b/api/src/main/java/org/apache/unomi/api/actions/Action.java
index b3505bbe8..29c6927e1 100644
--- a/api/src/main/java/org/apache/unomi/api/actions/Action.java
+++ b/api/src/main/java/org/apache/unomi/api/actions/Action.java
@@ -18,18 +18,26 @@
 package org.apache.unomi.api.actions;
 
 import org.apache.unomi.api.rules.Rule;
+import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
 
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlTransient;
 import java.io.Serializable;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
+
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
 
 /**
  * An action that can be executed as a consequence of a {@link Rule} being 
triggered. An action is characterized by its associated {@link
  * ActionType} and parameter values.
  */
-public class Action implements Serializable {
+public class Action implements Serializable, YamlConvertible {
     private ActionType actionType;
     private String actionTypeId;
     private Map<String, Object> parameterValues = new HashMap<>();
@@ -117,4 +125,42 @@ public class Action implements Serializable {
         parameterValues.put(name, value);
     }
 
+    /**
+     * Converts this action to a Map structure for YAML output.
+     * Implements YamlConvertible interface with circular reference detection.
+     *
+     * @param visited set of already visited objects to prevent infinite 
recursion (may be null)
+     * @return a Map representation of this action
+     */
+    @Override
+    public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+        if (maxDepth <= 0) {
+            return YamlMapBuilder.create()
+                .put("type", actionTypeId != null ? actionTypeId : "Action")
+                .put("parameterValues", "<max depth exceeded>")
+                .build();
+        }
+        if (visited != null && visited.contains(this)) {
+            return circularRef();
+        }
+        final Set<Object> visitedSet = visited != null ? visited : new 
HashSet<>();
+        visitedSet.add(this);
+        try {
+            YamlMapBuilder builder = YamlMapBuilder.create()
+                .put("type", actionTypeId != null ? actionTypeId : "Action");
+            if (parameterValues != null && !parameterValues.isEmpty()) {
+                builder.put("parameterValues", toYamlValue(parameterValues, 
visitedSet, maxDepth - 1));
+            }
+            return builder.build();
+        } finally {
+            visitedSet.remove(this);
+        }
+    }
+
+    @Override
+    public String toString() {
+        Map<String, Object> map = toYaml();
+        return YamlUtils.format(map);
+    }
+
 }
diff --git a/api/src/main/java/org/apache/unomi/api/actions/ActionType.java 
b/api/src/main/java/org/apache/unomi/api/actions/ActionType.java
index fddf732f9..03c4e6e0b 100644
--- a/api/src/main/java/org/apache/unomi/api/actions/ActionType.java
+++ b/api/src/main/java/org/apache/unomi/api/actions/ActionType.java
@@ -21,14 +21,18 @@ import org.apache.unomi.api.Metadata;
 import org.apache.unomi.api.MetadataItem;
 import org.apache.unomi.api.Parameter;
 import org.apache.unomi.api.PluginType;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.util.*;
+
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
 
 /**
  * A type definition for {@link Action}s.
  */
-public class ActionType extends MetadataItem implements PluginType {
+public class ActionType extends MetadataItem implements PluginType, 
YamlConvertible {
     public static final String ITEM_TYPE = "actionType";
 
     private static final long serialVersionUID = -3522958600710010935L;
@@ -113,4 +117,36 @@ public class ActionType extends MetadataItem implements 
PluginType {
     public void setPluginId(long pluginId) {
         this.pluginId = pluginId;
     }
+
+    /**
+     * Converts this action type to a Map structure for YAML output.
+     * Implements YamlConvertible interface with circular reference detection.
+     *
+     * @param visited set of already visited objects to prevent infinite 
recursion (may be null)
+     * @return a Map representation of this action type
+     */
+    @Override
+    public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+        if (maxDepth <= 0) {
+            return YamlMapBuilder.create()
+                .put("parameters", "<max depth exceeded>")
+                .put("pluginId", pluginId)
+                .build();
+        }
+        if (visited != null && visited.contains(this)) {
+            return circularRef();
+        }
+        final Set<Object> visitedSet = visited != null ? visited : new 
HashSet<>();
+        visitedSet.add(this);
+        try {
+            return YamlMapBuilder.create()
+                .mergeObject(super.toYaml(visitedSet, maxDepth))
+                .putIfNotNull("actionExecutor", actionExecutor)
+                .putIfNotEmpty("parameters", parameters != null ? 
(Collection<?>) toYamlValue(parameters, visitedSet, maxDepth - 1) : null)
+                .put("pluginId", pluginId)
+                .build();
+        } finally {
+            visitedSet.remove(this);
+        }
+    }
 }
diff --git a/api/src/main/java/org/apache/unomi/api/conditions/Condition.java 
b/api/src/main/java/org/apache/unomi/api/conditions/Condition.java
index d41890bff..812ee5862 100644
--- a/api/src/main/java/org/apache/unomi/api/conditions/Condition.java
+++ b/api/src/main/java/org/apache/unomi/api/conditions/Condition.java
@@ -18,23 +18,21 @@
 package org.apache.unomi.api.conditions;
 
 import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
 import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
 
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlTransient;
 import java.io.Serializable;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.Collections;
+import java.util.*;
 
 import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
 
 /**
  * A set of elements that can be evaluated.
  */
-public class Condition implements Serializable {
+public class Condition implements Serializable, YamlConvertible {
     private static final long serialVersionUID = 7584522402785053206L;
 
     ConditionType conditionType;
@@ -133,7 +131,7 @@ public class Condition implements Serializable {
      * @return the value of the specified parameter or {@code null} if no such 
parameter exists
      */
     public Object getParameter(String name) {
-        return parameterValues.get(name);
+        return parameterValues != null ? parameterValues.get(name) : null;
     }
 
     /**
@@ -167,41 +165,95 @@ public class Condition implements Serializable {
     }
 
     /**
-     * Converts this condition to a Map structure for YAML output.
+     * Converts this condition to a Map structure for YAML output with depth 
limiting.
+     * Implements YamlConvertible interface with circular reference detection 
and depth limiting
+     * to prevent StackOverflowError from extremely deep nested structures.
      *
-     * @param visited set of already visited conditions to prevent infinite 
recursion
+     * @param visited set of already visited objects to prevent infinite 
recursion (may be null)
+     * @param maxDepth maximum recursion depth (prevents StackOverflowError 
from deep nesting)
      * @return a Map representation of this condition
      */
-    public Map<String, Object> toYaml(Set<Condition> visited) {
-        if (visited.contains(this)) {
+    @Override
+    public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+        if (maxDepth <= 0) {
+            return YamlMapBuilder.create()
+                .put("type", conditionTypeId != null ? conditionTypeId : 
"Condition")
+                .put("parameterValues", "<max depth exceeded>")
+                .build();
+        }
+        if (visited != null && visited.contains(this)) {
             return circularRef();
         }
-        visited.add(this);
+        final Set<Object> visitedSet = visited != null ? visited : new 
HashSet<>();
+        visitedSet.add(this);
         try {
             YamlMapBuilder builder = YamlMapBuilder.create()
                 .put("type", conditionTypeId != null ? conditionTypeId : 
"Condition");
             if (parameterValues != null && !parameterValues.isEmpty()) {
-                parameterValues.forEach((name, value) ->
-                    builder.put(name, toYamlValue(value, visited)));
+                builder.put("parameterValues", toYamlValue(parameterValues, 
visitedSet, maxDepth - 1));
             }
             return builder.build();
         } finally {
-            visited.remove(this);
+            visitedSet.remove(this);
         }
     }
 
-    private Object toYamlValue(Object value, Set<Condition> visited) {
-        if (value instanceof Condition) {
-            return ((Condition) value).toYaml(visited);
+    /**
+     * Creates a deep copy of this condition, including all nested conditions 
in parameter values.
+     * Recursively copies all nested conditions to avoid sharing references.
+     *
+     * @return a deep copy of this condition
+     */
+    public Condition deepCopy() {
+        Condition copied = new Condition();
+        if (this.conditionType != null) {
+            copied.setConditionType(this.conditionType);
+        } else if (this.conditionTypeId != null) {
+            copied.setConditionTypeId(this.conditionTypeId);
         }
-        // For non-Condition values, use empty visited set since 
YamlUtils.toYamlValue
-        // doesn't currently use it for circular reference detection
-        return YamlUtils.toYamlValue(value, Collections.emptySet());
+
+        // Deep copy parameter values
+        Map<String, Object> copiedParams = new HashMap<>();
+        if (this.parameterValues != null) {
+            for (Map.Entry<String, Object> entry : 
this.parameterValues.entrySet()) {
+                Object value = entry.getValue();
+                if (value instanceof Condition) {
+                    // Recursively deep copy nested condition
+                    copiedParams.put(entry.getKey(), ((Condition) 
value).deepCopy());
+                } else if (value instanceof Collection) {
+                    // Deep copy collection - preserve the collection type if 
possible
+                    Collection<?> collection = (Collection<?>) value;
+                    Collection<Object> copiedCollection;
+                    if (collection instanceof List) {
+                        copiedCollection = new ArrayList<>();
+                    } else {
+                        // Fallback to ArrayList for other collection types
+                        copiedCollection = new ArrayList<>();
+                    }
+                    for (Object item : collection) {
+                        if (item instanceof Condition) {
+                            // Recursively deep copy nested condition
+                            copiedCollection.add(((Condition) 
item).deepCopy());
+                        } else {
+                            // Not a condition, add as-is (for non-condition 
values in collections)
+                            copiedCollection.add(item);
+                        }
+                    }
+                    copiedParams.put(entry.getKey(), copiedCollection);
+                } else {
+                    // Primitive or other non-condition value, copy as-is
+                    copiedParams.put(entry.getKey(), value);
+                }
+            }
+        }
+        copied.setParameterValues(copiedParams);
+
+        return copied;
     }
 
     @Override
     public String toString() {
-        Map<String, Object> map = toYaml(new HashSet<>());
+        Map<String, Object> map = toYaml();
         return YamlUtils.format(map);
     }
 }
diff --git 
a/api/src/main/java/org/apache/unomi/api/conditions/ConditionType.java 
b/api/src/main/java/org/apache/unomi/api/conditions/ConditionType.java
index 621e3aab8..cd4683f65 100644
--- a/api/src/main/java/org/apache/unomi/api/conditions/ConditionType.java
+++ b/api/src/main/java/org/apache/unomi/api/conditions/ConditionType.java
@@ -21,17 +21,10 @@ import org.apache.unomi.api.Metadata;
 import org.apache.unomi.api.MetadataItem;
 import org.apache.unomi.api.Parameter;
 import org.apache.unomi.api.PluginType;
-import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.*;
 
 import javax.xml.bind.annotation.XmlElement;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import static org.apache.unomi.api.utils.YamlUtils.*;
+import java.util.*;
 
 /**
  * ConditionTypes define new conditions that can be applied to items (for 
example to decide whether a rule needs to be triggered or if a profile is 
considered as taking part in a
@@ -39,7 +32,7 @@ import static org.apache.unomi.api.utils.YamlUtils.*;
  * optimized by coding it. They may also be defined as combination of other 
conditions. A simple condition  could be: “User is male”, while a more generic 
condition with
  * parameters may test whether a given property has a specific value: “User 
property x has value y”.
  */
-public class ConditionType extends MetadataItem implements PluginType {
+public class ConditionType extends MetadataItem implements PluginType, 
YamlConvertible {
     public static final String ITEM_TYPE = "conditionType";
 
     private static final long serialVersionUID = -6965481691241954969L;
@@ -164,37 +157,36 @@ public class ConditionType extends MetadataItem 
implements PluginType {
 
     /**
      * Converts this condition type to a Map structure for YAML output.
+     * Implements YamlConvertible interface with circular reference detection.
      *
-     * @param visited set of already visited condition types to prevent 
infinite recursion
+     * @param visited set of already visited objects to prevent infinite 
recursion (may be null)
      * @return a Map representation of this condition type
      */
-    public Map<String, Object> toYaml(Set<ConditionType> visited) {
-        if (visited.contains(this)) {
+    @Override
+    public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+        if (maxDepth <= 0) {
+            return YamlMapBuilder.create()
+                .put("parentCondition", "<max depth exceeded>")
+                .put("parameters", "<max depth exceeded>")
+                .put("pluginId", pluginId)
+                .build();
+        }
+        if (visited != null && visited.contains(this)) {
             return circularRef();
         }
-        visited.add(this);
+        final Set<Object> visitedSet = visited != null ? visited : new 
HashSet<>();
+        visitedSet.add(this);
         try {
             return YamlMapBuilder.create()
-                .putIfNotNull("id", itemId)
+                .mergeObject(super.toYaml(visitedSet, maxDepth))
                 .putIfNotNull("conditionEvaluator", conditionEvaluator)
                 .putIfNotNull("queryBuilder", queryBuilder)
-                .putIfNotNull("parentCondition", parentCondition != null ? 
parentCondition.toYaml(new HashSet<>()) : null)
-                .putIfNotEmpty("parameters", parameters != null ? 
parameters.stream()
-                    .map(Parameter::toYaml)
-                    .collect(Collectors.toList()) : null)
+                .putIfNotNull("parentCondition", parentCondition != null ? 
toYamlValue(parentCondition, visitedSet, maxDepth - 1) : null)
+                .putIfNotEmpty("parameters", parameters != null ? 
(Collection<?>) toYamlValue(parameters, visitedSet, maxDepth - 1) : null)
                 .put("pluginId", pluginId)
-                .putIfNotNull("name", metadata != null ? metadata.getName() : 
null)
-                .putIfNotNull("description", metadata != null ? 
metadata.getDescription() : null)
-                .putIfNotNull("scope", metadata != null ? metadata.getScope() 
: null)
                 .build();
         } finally {
-            visited.remove(this);
+            visitedSet.remove(this);
         }
     }
-
-    @Override
-    public String toString() {
-        Map<String, Object> map = toYaml(new HashSet<>());
-        return YamlUtils.format(map);
-    }
 }
diff --git 
a/api/src/main/java/org/apache/unomi/api/conditions/ConditionValidation.java 
b/api/src/main/java/org/apache/unomi/api/conditions/ConditionValidation.java
index 10aa0cd21..6bcd9aa8a 100644
--- a/api/src/main/java/org/apache/unomi/api/conditions/ConditionValidation.java
+++ b/api/src/main/java/org/apache/unomi/api/conditions/ConditionValidation.java
@@ -17,17 +17,18 @@
 package org.apache.unomi.api.conditions;
 
 import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
 
 import java.io.Serializable;
 import java.util.Map;
 import java.util.Set;
 
-import static org.apache.unomi.api.utils.YamlUtils.*;
+import static org.apache.unomi.api.utils.YamlUtils.setToSortedList;
 
 /**
  * Validation metadata for condition parameters
  */
-public class ConditionValidation implements Serializable {
+public class ConditionValidation implements Serializable, YamlConvertible {
     private static final long serialVersionUID = 1L;
 
     public enum Type {
@@ -120,10 +121,13 @@ public class ConditionValidation implements Serializable {
 
     /**
      * Converts this validation to a Map structure for YAML output.
+     * Implements YamlConvertible interface.
      *
+     * @param visited set of already visited objects to prevent infinite 
recursion (may be null)
      * @return a Map representation of this validation
      */
-    public Map<String, Object> toYaml() {
+    @Override
+    public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
         return YamlUtils.YamlMapBuilder.create()
             .putIf("required", true, required)
             .putIf("recommended", true, recommended)
diff --git a/api/src/main/java/org/apache/unomi/api/goals/Goal.java 
b/api/src/main/java/org/apache/unomi/api/goals/Goal.java
index 1b2ff0e88..12694b3d7 100644
--- a/api/src/main/java/org/apache/unomi/api/goals/Goal.java
+++ b/api/src/main/java/org/apache/unomi/api/goals/Goal.java
@@ -21,6 +21,15 @@ import org.apache.unomi.api.Metadata;
 import org.apache.unomi.api.MetadataItem;
 import org.apache.unomi.api.campaigns.Campaign;
 import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
 
 /**
  * A tracked activity / action that can be accomplished by site (scope) 
visitors. These are tracked in general because they relate to specific business 
objectives or are
@@ -32,7 +41,7 @@ import org.apache.unomi.api.conditions.Condition;
  * <li>audience filtering: any visitor is considered for scope-level goals 
while campaign-level goals only consider visitors who match the campaign's 
conditions
  * </ul>
  */
-public class Goal extends MetadataItem {
+public class Goal extends MetadataItem implements YamlConvertible {
     public static final String ITEM_TYPE = "goal";
     private static final long serialVersionUID = 6131648013470949983L;
     private Condition startEvent;
@@ -87,4 +96,37 @@ public class Goal extends MetadataItem {
     public void setCampaignId(String campaignId) {
         this.campaignId = campaignId;
     }
+
+    /**
+     * Converts this goal to a Map structure for YAML output.
+     * Implements YamlConvertible interface with circular reference detection.
+     *
+     * @param visited set of already visited objects to prevent infinite 
recursion (may be null)
+     * @return a Map representation of this goal
+     */
+    @Override
+    public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+        if (maxDepth <= 0) {
+            return YamlMapBuilder.create()
+                .put("startEvent", "<max depth exceeded>")
+                .put("targetEvent", "<max depth exceeded>")
+                .put("campaignId", campaignId)
+                .build();
+        }
+        if (visited != null && visited.contains(this)) {
+            return circularRef();
+        }
+        final Set<Object> visitedSet = visited != null ? visited : new 
HashSet<>();
+        visitedSet.add(this);
+        try {
+            return YamlMapBuilder.create()
+                .mergeObject(super.toYaml(visitedSet, maxDepth))
+                .putIfNotNull("startEvent", startEvent != null ? 
toYamlValue(startEvent, visitedSet, maxDepth - 1) : null)
+                .putIfNotNull("targetEvent", targetEvent != null ? 
toYamlValue(targetEvent, visitedSet, maxDepth - 1) : null)
+                .putIfNotNull("campaignId", campaignId)
+                .build();
+        } finally {
+            visitedSet.remove(this);
+        }
+    }
 }
diff --git a/api/src/main/java/org/apache/unomi/api/rules/Rule.java 
b/api/src/main/java/org/apache/unomi/api/rules/Rule.java
index 0aedfb123..63750c0d4 100644
--- a/api/src/main/java/org/apache/unomi/api/rules/Rule.java
+++ b/api/src/main/java/org/apache/unomi/api/rules/Rule.java
@@ -20,10 +20,15 @@ package org.apache.unomi.api.rules;
 import org.apache.unomi.api.*;
 import org.apache.unomi.api.actions.Action;
 import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
 
-import java.util.List;
+import java.util.*;
 import java.util.stream.Collectors;
 
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
+
 /**
  * A conditional set of actions to be executed in response to incoming events. 
Triggering of rules is guarded by a condition: the rule is only triggered if 
the associated
  * condition ({@link #getCondition()}) is satisfied. Once a rule triggers, a 
list of actions ({@link #getActions()} can be performed as consequences.
@@ -34,7 +39,7 @@ import java.util.stream.Collectors;
  * We could also specify a priority for our rule in case it needs to be 
executed before other ones when similar conditions match. This is accomplished 
using the
  * {@link #getPriority()} property.
  */
-public class Rule extends MetadataItem {
+public class Rule extends MetadataItem implements YamlConvertible {
 
     /**
      * The Rule ITEM_TYPE.
@@ -197,4 +202,41 @@ public class Rule extends MetadataItem {
     public void setPriority(int priority) {
         this.priority = priority;
     }
+
+    /**
+     * Converts this rule to a Map structure for YAML output.
+     * Implements YamlConvertible interface with circular reference detection.
+     *
+     * @param visited set of already visited objects to prevent infinite 
recursion (may be null)
+     * @return a Map representation of this rule
+     */
+    @Override
+    public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+        if (maxDepth <= 0) {
+            return YamlMapBuilder.create()
+                .put("condition", "<max depth exceeded>")
+                .put("actions", "<max depth exceeded>")
+                .put("priority", priority)
+                .build();
+        }
+        if (visited != null && visited.contains(this)) {
+            return circularRef();
+        }
+        final Set<Object> visitedSet = visited != null ? visited : new 
HashSet<>();
+        visitedSet.add(this);
+        try {
+            return YamlMapBuilder.create()
+                .mergeObject(super.toYaml(visitedSet, maxDepth))
+                .putIfNotNull("condition", condition != null ? 
toYamlValue(condition, visitedSet, maxDepth - 1) : null)
+                .putIfNotEmpty("actions", actions != null ? (Collection<?>) 
toYamlValue(actions, visitedSet, maxDepth - 1) : null)
+                .putIfNotEmpty("linkedItems", linkedItems)
+                .putIf("raiseEventOnlyOnceForProfile", true, 
raiseEventOnlyOnceForProfile)
+                .putIf("raiseEventOnlyOnceForSession", true, 
raiseEventOnlyOnceForSession)
+                .putIf("raiseEventOnlyOnce", true, raiseEventOnlyOnce)
+                .putIf("priority", priority, priority != 0)
+                .build();
+        } finally {
+            visitedSet.remove(this);
+        }
+    }
 }
diff --git a/api/src/main/java/org/apache/unomi/api/segments/Scoring.java 
b/api/src/main/java/org/apache/unomi/api/segments/Scoring.java
index 6018fb376..3d82e6e7e 100644
--- a/api/src/main/java/org/apache/unomi/api/segments/Scoring.java
+++ b/api/src/main/java/org/apache/unomi/api/segments/Scoring.java
@@ -21,14 +21,19 @@ import org.apache.unomi.api.Item;
 import org.apache.unomi.api.Metadata;
 import org.apache.unomi.api.MetadataItem;
 import org.apache.unomi.api.Profile;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
 
-import java.util.List;
+import java.util.*;
+
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
 
 /**
  * A set of conditions associated with a value to assign to {@link Profile}s 
when matching so that the associated users can be scored along that
  * dimension. Each {@link ScoringElement} is evaluated and matching profiles' 
scores are incremented with the associated value.
  */
-public class Scoring extends MetadataItem {
+public class Scoring extends MetadataItem implements YamlConvertible {
     /**
      * The Scoring ITEM_TYPE.
      *
@@ -71,4 +76,33 @@ public class Scoring extends MetadataItem {
         this.elements = elements;
     }
 
+    /**
+     * Converts this scoring to a Map structure for YAML output.
+     * Implements YamlConvertible interface with circular reference detection.
+     *
+     * @param visited set of already visited objects to prevent infinite 
recursion (may be null)
+     * @return a Map representation of this scoring
+     */
+    @Override
+    public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+        if (maxDepth <= 0) {
+            return YamlMapBuilder.create()
+                .put("elements", "<max depth exceeded>")
+                .build();
+        }
+        if (visited != null && visited.contains(this)) {
+            return circularRef();
+        }
+        final Set<Object> visitedSet = visited != null ? visited : new 
HashSet<>();
+        visitedSet.add(this);
+        try {
+            return YamlMapBuilder.create()
+                .mergeObject(super.toYaml(visitedSet, maxDepth))
+                .putIfNotEmpty("elements", elements != null ? (Collection<?>) 
toYamlValue(elements, visitedSet, maxDepth - 1) : null)
+                .build();
+        } finally {
+            visitedSet.remove(this);
+        }
+    }
+
 }
diff --git 
a/api/src/main/java/org/apache/unomi/api/segments/ScoringElement.java 
b/api/src/main/java/org/apache/unomi/api/segments/ScoringElement.java
index 1af5147fb..bfc5a21cc 100644
--- a/api/src/main/java/org/apache/unomi/api/segments/ScoringElement.java
+++ b/api/src/main/java/org/apache/unomi/api/segments/ScoringElement.java
@@ -18,13 +18,22 @@
 package org.apache.unomi.api.segments;
 
 import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.utils.YamlUtils;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
 
 import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
 
 /**
  * A scoring dimension along profiles can be evaluated and associated value to 
be assigned.
  */
-public class ScoringElement implements Serializable {
+public class ScoringElement implements Serializable, YamlConvertible {
     private Condition condition;
     private int value;
 
@@ -69,4 +78,40 @@ public class ScoringElement implements Serializable {
     public void setValue(int value) {
         this.value = value;
     }
+
+    /**
+     * Converts this scoring element to a Map structure for YAML output.
+     * Implements YamlConvertible interface with circular reference detection.
+     *
+     * @param visited set of already visited objects to prevent infinite 
recursion (may be null)
+     * @return a Map representation of this scoring element
+     */
+    @Override
+    public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+        if (maxDepth <= 0) {
+            return YamlMapBuilder.create()
+                .put("condition", "<max depth exceeded>")
+                .put("value", value)
+                .build();
+        }
+        if (visited != null && visited.contains(this)) {
+            return circularRef();
+        }
+        final Set<Object> visitedSet = visited != null ? visited : new 
HashSet<>();
+        visitedSet.add(this);
+        try {
+            return YamlMapBuilder.create()
+                .putIfNotNull("condition", condition != null ? 
toYamlValue(condition, visitedSet, maxDepth - 1) : null)
+                .put("value", value)
+                .build();
+        } finally {
+            visitedSet.remove(this);
+        }
+    }
+
+    @Override
+    public String toString() {
+        Map<String, Object> map = toYaml();
+        return YamlUtils.format(map);
+    }
 }
diff --git a/api/src/main/java/org/apache/unomi/api/segments/Segment.java 
b/api/src/main/java/org/apache/unomi/api/segments/Segment.java
index 4e0d33830..2729ec74d 100644
--- a/api/src/main/java/org/apache/unomi/api/segments/Segment.java
+++ b/api/src/main/java/org/apache/unomi/api/segments/Segment.java
@@ -22,13 +22,22 @@ import org.apache.unomi.api.Metadata;
 import org.apache.unomi.api.MetadataItem;
 import org.apache.unomi.api.Profile;
 import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.utils.YamlUtils.YamlConvertible;
+import org.apache.unomi.api.utils.YamlUtils.YamlMapBuilder;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static org.apache.unomi.api.utils.YamlUtils.circularRef;
+import static org.apache.unomi.api.utils.YamlUtils.toYamlValue;
 
 /**
  * A dynamically evaluated group of similar profiles in order to categorize 
the associated users. To be considered part of a given segment, users must 
satisfies
  * the segment’s condition. If they match, users are automatically added to 
the segment. Similarly, if at any given point during, they cease to satisfy the 
segment’s condition,
  * they are automatically removed from it.
  */
-public class Segment extends MetadataItem {
+public class Segment extends MetadataItem implements YamlConvertible {
 
     /**
      * The Segment ITEM_TYPE.
@@ -72,4 +81,33 @@ public class Segment extends MetadataItem {
         this.condition = condition;
     }
 
+    /**
+     * Converts this segment to a Map structure for YAML output.
+     * Implements YamlConvertible interface with circular reference detection.
+     *
+     * @param visited set of already visited objects to prevent infinite 
recursion (may be null)
+     * @return a Map representation of this segment
+     */
+    @Override
+    public Map<String, Object> toYaml(Set<Object> visited, int maxDepth) {
+        if (maxDepth <= 0) {
+            return YamlMapBuilder.create()
+                .put("condition", "<max depth exceeded>")
+                .build();
+        }
+        if (visited != null && visited.contains(this)) {
+            return circularRef();
+        }
+        final Set<Object> visitedSet = visited != null ? visited : new 
HashSet<>();
+        visitedSet.add(this);
+        try {
+            return YamlMapBuilder.create()
+                .mergeObject(super.toYaml(visitedSet, maxDepth))
+                .putIfNotNull("condition", condition != null ? 
toYamlValue(condition, visitedSet, maxDepth - 1) : null)
+                .build();
+        } finally {
+            visitedSet.remove(this);
+        }
+    }
+
 }
diff --git a/api/src/main/java/org/apache/unomi/api/utils/YamlUtils.java 
b/api/src/main/java/org/apache/unomi/api/utils/YamlUtils.java
index 4dd4cce85..664f63fb7 100644
--- a/api/src/main/java/org/apache/unomi/api/utils/YamlUtils.java
+++ b/api/src/main/java/org/apache/unomi/api/utils/YamlUtils.java
@@ -48,12 +48,38 @@ public class YamlUtils {
      * Interface for objects that can convert themselves to YAML Map 
structures.
      */
     public interface YamlConvertible {
+        /**
+         * Converts this object to a Map structure for YAML output with depth 
limiting.
+         * This method accepts an optional visited set to detect circular 
references and a max depth
+         * to prevent StackOverflowError from extremely deep nested structures.
+         *
+         * @param visited optional set of visited objects to detect circular 
references (may be null)
+         * @param maxDepth maximum recursion depth (prevents 
StackOverflowError from deep nesting)
+         * @return a Map representation of this object
+         */
+        Map<String, Object> toYaml(Set<Object> visited, int maxDepth);
+
         /**
          * Converts this object to a Map structure for YAML output.
+         * This method accepts an optional visited set to detect circular 
references.
+         * Uses a default max depth of 20 to prevent StackOverflowError.
          *
+         * @param visited optional set of visited objects to detect circular 
references (may be null)
          * @return a Map representation of this object
          */
-        Map<String, Object> toYaml();
+        default Map<String, Object> toYaml(Set<Object> visited) {
+            return toYaml(visited, 20);
+        }
+
+        /**
+         * Converts this object to a Map structure for YAML output.
+         * This is a convenience method that calls toYaml(null, 20).
+         *
+         * @return a Map representation of this object
+         */
+        default Map<String, Object> toYaml() {
+            return toYaml(null, 20);
+        }
     }
 
     /**
@@ -147,6 +173,28 @@ public class YamlUtils {
             return this;
         }
 
+        /**
+         * Merges all fields from a Map into this builder.
+         * This is useful for inheritance where subclasses want to include 
parent class fields.
+         * 
+         * Usage in subclasses:
+         * <pre>
+         * return YamlMapBuilder.create()
+         *     .mergeObject(super.toYaml(visitedSet))
+         *     .putIfNotNull("field", value)
+         *     .build();
+         * </pre>
+         *
+         * @param objectMap the Map containing fields to merge (may be null, 
in which case nothing is merged)
+         * @return this builder for chaining
+         */
+        public YamlMapBuilder mergeObject(Map<String, Object> objectMap) {
+            if (objectMap != null) {
+                objectMap.forEach(map::put);
+            }
+            return this;
+        }
+
         /**
          * Builds and returns a defensive copy of the map.
          *
@@ -190,35 +238,63 @@ public class YamlUtils {
 
     /**
      * Converts a value to YAML-compatible format, handling nested structures.
-     * Note: This method does not perform circular reference detection for 
generic objects.
-     * For objects that implement YamlConvertible, circular reference 
detection should be
-     * handled in their toYaml() implementation.
+     * For objects that implement YamlConvertible, circular reference 
detection is
+     * handled by passing the visited set to their toYaml() implementation.
      *
      * @param value the value to convert
-     * @param visited set of visited objects (currently unused, reserved for 
future circular reference detection)
+     * @param visited set of visited objects for circular reference detection 
(may be null)
      * @return the converted value
      */
     public static Object toYamlValue(Object value, Set<Object> visited) {
+        return toYamlValue(value, visited, Integer.MAX_VALUE);
+    }
+
+    /**
+     * Converts a value to YAML-compatible format with depth limiting to 
prevent StackOverflowError.
+     * For objects that implement YamlConvertible, circular reference 
detection is
+     * handled by passing the visited set to their toYaml() implementation.
+     *
+     * @param value the value to convert
+     * @param visited set of visited objects for circular reference detection 
(may be null)
+     * @param maxDepth maximum recursion depth (prevents StackOverflowError 
from deep nesting)
+     * @return the converted value, or a placeholder if max depth exceeded
+     */
+    public static Object toYamlValue(Object value, Set<Object> visited, int 
maxDepth) {
+        if (maxDepth <= 0) {
+            return "<max depth exceeded>";
+        }
         if (value == null) {
             return null;
         }
         if (value instanceof YamlConvertible) {
-            return ((YamlConvertible) value).toYaml();
+            // For YamlConvertible, get the Map and then process it as a Map 
to ensure sorting
+            // Pass maxDepth - 1 to the toYaml method to continue depth 
limiting
+            Map<String, Object> result = ((YamlConvertible) 
value).toYaml(visited, maxDepth - 1);
+            // Process the result as a Map to ensure it's sorted (this handles 
both sorting and recursive processing)
+            return toYamlValue(result, visited, maxDepth - 1);
         }
         if (value instanceof List) {
             return ((List<?>) value).stream()
-                .map(item -> toYamlValue(item, visited))
+                .map(item -> toYamlValue(item, visited, maxDepth - 1))
                 .collect(Collectors.toList());
         }
         if (value instanceof Map) {
+            Map<?, ?> inputMap = (Map<?, ?>) value;
             Map<String, Object> result = new LinkedHashMap<>();
-            ((Map<?, ?>) value).forEach((key, val) ->
-                result.put(String.valueOf(key), toYamlValue(val, visited)));
+            
+            if (!inputMap.isEmpty()) {
+                // Sort entries alphabetically by key string representation
+                inputMap.entrySet().stream()
+                    .sorted((e1, e2) -> 
String.valueOf(e1.getKey()).compareTo(String.valueOf(e2.getKey())))
+                    .forEach(entry ->
+                        result.put(String.valueOf(entry.getKey()), 
toYamlValue(entry.getValue(), visited, maxDepth - 1)));
+            }
             return result;
         }
         return value;
     }
 
+
     /**
      * Formats a value as YAML using SnakeYaml.
      * This is a convenience method that delegates to SnakeYaml.
diff --git a/api/src/test/java/org/apache/unomi/api/utils/YamlUtilsTest.java 
b/api/src/test/java/org/apache/unomi/api/utils/YamlUtilsTest.java
index 95a00e523..c40c066ca 100644
--- a/api/src/test/java/org/apache/unomi/api/utils/YamlUtilsTest.java
+++ b/api/src/test/java/org/apache/unomi/api/utils/YamlUtilsTest.java
@@ -16,6 +16,10 @@
  */
 package org.apache.unomi.api.utils;
 
+import org.apache.unomi.api.Metadata;
+import org.apache.unomi.api.actions.Action;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.rules.Rule;
 import org.junit.Test;
 
 import java.util.*;
@@ -189,7 +193,7 @@ public class YamlUtilsTest {
 
     @Test
     public void testToYamlValueWithYamlConvertible() {
-        YamlUtils.YamlConvertible convertible = () -> {
+        YamlUtils.YamlConvertible convertible = (visited, maxDepth) -> {
             Map<String, Object> map = new LinkedHashMap<>();
             map.put("test", "value");
             return map;
@@ -252,4 +256,355 @@ public class YamlUtilsTest {
         assertTrue("Format should contain key", result.contains("key"));
         assertTrue("Format should contain value", result.contains("value"));
     }
+
+    // ========== Circular Reference Detection Tests ==========
+
+    @Test
+    public void testRuleInheritanceChainNoCircularRef() {
+        // Test that Rule -> MetadataItem -> Item inheritance chain doesn't 
produce false circular refs
+        Rule rule = new Rule();
+        rule.setItemId("test-rule");
+        Metadata metadata = new Metadata("test-rule");
+        metadata.setScope("systemscope");
+        rule.setMetadata(metadata);
+        
+        Condition condition = new Condition();
+        condition.setConditionTypeId("testCondition");
+        rule.setCondition(condition);
+        
+        Map<String, Object> result = rule.toYaml(null);
+        assertNotNull("Rule should serialize to YAML", result);
+        assertFalse("Should not contain circular reference marker", 
result.containsKey("$ref"));
+        assertTrue("Should contain condition", 
result.containsKey("condition"));
+        assertTrue("Should contain itemId from Item parent", 
result.containsKey("itemId"));
+        assertTrue("Should contain metadata from MetadataItem parent", 
result.containsKey("metadata"));
+    }
+
+    @Test
+    public void testRuleWithCircularReferenceInCondition() {
+        // Test that a real circular reference (Rule referenced in condition's 
parameterValues) is detected
+        Rule rule = new Rule();
+        rule.setItemId("test-rule");
+        Metadata metadata = new Metadata("test-rule");
+        rule.setMetadata(metadata);
+        
+        Condition condition = new Condition();
+        condition.setConditionTypeId("testCondition");
+        // Create a circular reference: condition's parameterValues contains 
the rule itself
+        condition.getParameterValues().put("referencedRule", rule);
+        rule.setCondition(condition);
+        
+        Map<String, Object> result = rule.toYaml(null);
+        assertNotNull("Rule should serialize to YAML", result);
+        assertTrue("Should contain condition", 
result.containsKey("condition"));
+        
+        // Check that the circular reference is detected in the condition's 
parameterValues
+        Map<String, Object> conditionMap = (Map<String, Object>) 
result.get("condition");
+        assertNotNull("Condition should be serialized", conditionMap);
+        Map<String, Object> paramValues = (Map<String, Object>) 
conditionMap.get("parameterValues");
+        assertNotNull("Parameter values should exist", paramValues);
+        Map<String, Object> circularRef = (Map<String, Object>) 
paramValues.get("referencedRule");
+        assertNotNull("Circular reference should be detected", circularRef);
+        assertEquals("Should contain circular reference marker", "circular", 
circularRef.get("$ref"));
+    }
+
+    @Test
+    public void testRuleWithCircularReferenceInActions() {
+        // Test circular reference in actions list
+        Rule rule = new Rule();
+        rule.setItemId("test-rule");
+        Metadata metadata = new Metadata("test-rule");
+        rule.setMetadata(metadata);
+        
+        Action action = new Action();
+        action.setActionTypeId("testAction");
+        // Create circular reference: action's parameterValues contains the 
rule
+        action.getParameterValues().put("triggeringRule", rule);
+        rule.setActions(Collections.singletonList(action));
+        
+        Map<String, Object> result = rule.toYaml(null);
+        assertNotNull("Rule should serialize to YAML", result);
+        assertTrue("Should contain actions", result.containsKey("actions"));
+        
+        List<?> actions = (List<?>) result.get("actions");
+        assertNotNull("Actions list should exist", actions);
+        assertEquals("Should have one action", 1, actions.size());
+        
+        Map<String, Object> actionMap = (Map<String, Object>) actions.get(0);
+        Map<String, Object> paramValues = (Map<String, Object>) 
actionMap.get("parameterValues");
+        assertNotNull("Parameter values should exist", paramValues);
+        Map<String, Object> circularRef = (Map<String, Object>) 
paramValues.get("triggeringRule");
+        assertNotNull("Circular reference should be detected", circularRef);
+        assertEquals("Should contain circular reference marker", "circular", 
circularRef.get("$ref"));
+    }
+
+    @Test
+    public void testNestedCircularReference() {
+        // Test nested circular reference: Rule -> Condition -> nested 
Condition -> Rule
+        Rule rule = new Rule();
+        rule.setItemId("test-rule");
+        Metadata metadata = new Metadata("test-rule");
+        rule.setMetadata(metadata);
+        
+        Condition outerCondition = new Condition();
+        outerCondition.setConditionTypeId("outerCondition");
+        
+        Condition nestedCondition = new Condition();
+        nestedCondition.setConditionTypeId("nestedCondition");
+        // Nested condition references the rule
+        nestedCondition.getParameterValues().put("ruleRef", rule);
+        
+        // Outer condition contains nested condition
+        outerCondition.getParameterValues().put("nested", nestedCondition);
+        rule.setCondition(outerCondition);
+        
+        Map<String, Object> result = rule.toYaml(null);
+        assertNotNull("Rule should serialize to YAML", result);
+        
+        // Navigate through the nested structure
+        Map<String, Object> conditionMap = (Map<String, Object>) 
result.get("condition");
+        Map<String, Object> paramValues = (Map<String, Object>) 
conditionMap.get("parameterValues");
+        Map<String, Object> nestedConditionMap = (Map<String, Object>) 
paramValues.get("nested");
+        Map<String, Object> nestedParamValues = (Map<String, Object>) 
nestedConditionMap.get("parameterValues");
+        Map<String, Object> circularRef = (Map<String, Object>) 
nestedParamValues.get("ruleRef");
+        
+        assertNotNull("Circular reference should be detected in nested 
structure", circularRef);
+        assertEquals("Should contain circular reference marker", "circular", 
circularRef.get("$ref"));
+    }
+
+    @Test
+    public void testMultipleCircularReferences() {
+        // Test multiple circular references to the same object
+        Rule rule = new Rule();
+        rule.setItemId("test-rule");
+        Metadata metadata = new Metadata("test-rule");
+        rule.setMetadata(metadata);
+        
+        Condition condition = new Condition();
+        condition.setConditionTypeId("testCondition");
+        // Multiple references to the same rule
+        condition.getParameterValues().put("rule1", rule);
+        condition.getParameterValues().put("rule2", rule);
+        condition.getParameterValues().put("rule3", rule);
+        rule.setCondition(condition);
+        
+        Map<String, Object> result = rule.toYaml(null);
+        Map<String, Object> conditionMap = (Map<String, Object>) 
result.get("condition");
+        Map<String, Object> paramValues = (Map<String, Object>) 
conditionMap.get("parameterValues");
+        
+        // All three references should show circular ref
+        for (String key : Arrays.asList("rule1", "rule2", "rule3")) {
+            Map<String, Object> circularRef = (Map<String, Object>) 
paramValues.get(key);
+            assertNotNull("Circular reference should be detected for " + key, 
circularRef);
+            assertEquals("Should contain circular reference marker for " + 
key, "circular", circularRef.get("$ref"));
+        }
+    }
+
+
+    @Test
+    public void testCircularReferenceInList() {
+        // Test circular reference in a list
+        Rule rule = new Rule();
+        rule.setItemId("test-rule");
+        Metadata metadata = new Metadata("test-rule");
+        rule.setMetadata(metadata);
+        
+        Condition condition = new Condition();
+        condition.setConditionTypeId("testCondition");
+        // List containing the rule itself
+        condition.getParameterValues().put("ruleList", Arrays.asList(rule, 
"other", rule));
+        rule.setCondition(condition);
+        
+        Map<String, Object> result = rule.toYaml(null);
+        Map<String, Object> conditionMap = (Map<String, Object>) 
result.get("condition");
+        Map<String, Object> paramValues = (Map<String, Object>) 
conditionMap.get("parameterValues");
+        List<?> ruleList = (List<?>) paramValues.get("ruleList");
+        
+        assertNotNull("Rule list should exist", ruleList);
+        assertEquals("List should have 3 elements", 3, ruleList.size());
+        
+        // First element should be circular ref
+        Map<String, Object> circularRef1 = (Map<String, Object>) 
ruleList.get(0);
+        assertEquals("First element should be circular ref", "circular", 
circularRef1.get("$ref"));
+        
+        // Second element should be string
+        assertEquals("Second element should be string", "other", 
ruleList.get(1));
+        
+        // Third element should also be circular ref
+        Map<String, Object> circularRef2 = (Map<String, Object>) 
ruleList.get(2);
+        assertEquals("Third element should be circular ref", "circular", 
circularRef2.get("$ref"));
+    }
+
+    @Test
+    public void testCircularReferenceInNestedMap() {
+        // Test circular reference in nested map structure
+        Rule rule = new Rule();
+        rule.setItemId("test-rule");
+        Metadata metadata = new Metadata("test-rule");
+        rule.setMetadata(metadata);
+        
+        Condition condition = new Condition();
+        condition.setConditionTypeId("testCondition");
+        // Nested map containing the rule
+        Map<String, Object> nestedMap = new HashMap<>();
+        nestedMap.put("level1", new HashMap<String, Object>() {{
+            put("level2", new HashMap<String, Object>() {{
+                put("rule", rule);
+            }});
+        }});
+        condition.getParameterValues().put("nested", nestedMap);
+        rule.setCondition(condition);
+        
+        Map<String, Object> result = rule.toYaml(null);
+        Map<String, Object> conditionMap = (Map<String, Object>) 
result.get("condition");
+        Map<String, Object> paramValues = (Map<String, Object>) 
conditionMap.get("parameterValues");
+        Map<String, Object> nested = (Map<String, Object>) 
paramValues.get("nested");
+        Map<String, Object> level1 = (Map<String, Object>) 
nested.get("level1");
+        Map<String, Object> level2 = (Map<String, Object>) 
level1.get("level2");
+        Map<String, Object> circularRef = (Map<String, Object>) 
level2.get("rule");
+        
+        assertNotNull("Circular reference should be detected in nested map", 
circularRef);
+        assertEquals("Should contain circular reference marker", "circular", 
circularRef.get("$ref"));
+    }
+
+    @Test
+    public void testNoFalseCircularRefInInheritance() {
+        // Test that inheritance chain (Rule -> MetadataItem -> Item) doesn't 
create false circular refs
+        // This is the main bug we're fixing
+        Rule rule = new Rule();
+        rule.setItemId("test-rule");
+        Metadata metadata = new Metadata("test-rule");
+        metadata.setScope("systemscope");
+        rule.setMetadata(metadata);
+        
+        Condition condition = new Condition();
+        condition.setConditionTypeId("unavailableConditionType");
+        condition.getParameterValues().put("comparisonOperator", "equals");
+        condition.getParameterValues().put("propertyName", "testProperty");
+        condition.getParameterValues().put("propertyValue", "testValue");
+        rule.setCondition(condition);
+        
+        Action action = new Action();
+        action.setActionTypeId("test");
+        rule.setActions(Collections.singletonList(action));
+        
+        Map<String, Object> result = rule.toYaml(null);
+        
+        // Should NOT contain $ref: circular at the top level
+        assertNotNull("Rule should serialize", result);
+        assertFalse("Should not have false circular reference at top level", 
+                    result.containsKey("$ref") && 
"circular".equals(result.get("$ref")));
+        
+        // Should contain all expected fields from inheritance chain
+        assertTrue("Should contain itemId from Item", 
result.containsKey("itemId"));
+        assertTrue("Should contain itemType from Item", 
result.containsKey("itemType"));
+        assertEquals("itemType should be 'rule'", "rule", 
result.get("itemType"));
+        assertTrue("Should contain metadata from MetadataItem", 
result.containsKey("metadata"));
+        assertTrue("Should contain condition", 
result.containsKey("condition"));
+        assertTrue("Should contain actions", result.containsKey("actions"));
+        
+        // Verify condition structure
+        Map<String, Object> conditionMap = (Map<String, Object>) 
result.get("condition");
+        assertNotNull("Condition should be present", conditionMap);
+        assertEquals("Condition should have correct type", 
"unavailableConditionType", conditionMap.get("type"));
+        
+        // Verify actions structure
+        List<?> actions = (List<?>) result.get("actions");
+        assertNotNull("Actions should be present", actions);
+        assertEquals("Should have one action", 1, actions.size());
+    }
+
+    @Test
+    public void testItemTypeIsAlwaysIncluded() {
+        // Test that itemType is always included in YAML output, even if null
+        // This reflects the actual state of the object
+        Rule rule = new Rule();
+        Metadata metadata = new Metadata("test-id");
+        metadata.setScope("systemscope");
+        rule.setMetadata(metadata);
+        
+        Map<String, Object> result = rule.toYaml(null);
+        
+        // itemType should always be present in output (set in Item 
constructor for Rule)
+        assertTrue("itemType should be included", 
result.containsKey("itemType"));
+        assertEquals("itemType should be 'rule'", "rule", 
result.get("itemType"));
+        
+        // itemId should also always be included
+        assertTrue("itemId should be included", result.containsKey("itemId"));
+    }
+
+    @Test
+    public void testItemIdAndItemTypeIncludedEvenWhenNull() {
+        // Test that itemId and itemType are always included, even when null
+        // This ensures YAML output reflects the actual state of the object
+        Rule rule = new Rule();
+        // Explicitly set itemId and itemType to null to test null handling
+        rule.setItemId(null);
+        rule.setItemType(null);
+        
+        Map<String, Object> result = rule.toYaml(null);
+        
+        // Both should be included even if null
+        assertTrue("itemId should be included even when null", 
result.containsKey("itemId"));
+        assertNull("itemId should be null", result.get("itemId"));
+        
+        assertTrue("itemType should be included even when null", 
result.containsKey("itemType"));
+        assertNull("itemType should be null", result.get("itemType"));
+    }
+
+    @Test
+    public void testItemIdFromMetadata() {
+        // Test that itemId is set from metadata and included in YAML
+        Rule rule = new Rule();
+        Metadata metadata = new Metadata("test-rule-id");
+        metadata.setScope("systemscope");
+        rule.setMetadata(metadata);
+        
+        Map<String, Object> result = rule.toYaml(null);
+        
+        // itemId should be set from metadata.getId()
+        assertTrue("itemId should be included when set from metadata", 
result.containsKey("itemId"));
+        assertEquals("itemId should match metadata id", "test-rule-id", 
result.get("itemId"));
+    }
+
+    @Test
+    public void testVisitedSetIsSharedCorrectly() {
+        // Test that visited set is properly shared across nested calls
+        Rule rule1 = new Rule();
+        rule1.setItemId("rule1");
+        rule1.setMetadata(new Metadata("rule1"));
+        
+        Rule rule2 = new Rule();
+        rule2.setItemId("rule2");
+        rule2.setMetadata(new Metadata("rule2"));
+        
+        // rule1 references rule2, rule2 references rule1 (mutual circular 
reference)
+        Condition condition1 = new Condition();
+        condition1.setConditionTypeId("test");
+        condition1.getParameterValues().put("otherRule", rule2);
+        rule1.setCondition(condition1);
+        
+        Condition condition2 = new Condition();
+        condition2.setConditionTypeId("test");
+        condition2.getParameterValues().put("otherRule", rule1);
+        rule2.setCondition(condition2);
+        
+        // Serialize rule1 - should detect circular ref when it encounters 
rule2 which references rule1
+        Map<String, Object> result1 = rule1.toYaml(null);
+        assertNotNull("Rule1 should serialize", result1);
+        
+        Map<String, Object> conditionMap1 = (Map<String, Object>) 
result1.get("condition");
+        Map<String, Object> paramValues1 = (Map<String, Object>) 
conditionMap1.get("parameterValues");
+        Map<String, Object> rule2Ref = (Map<String, Object>) 
paramValues1.get("otherRule");
+        
+        // rule2 should be serialized, but when it tries to reference rule1, 
it should detect circular ref
+        assertNotNull("Rule2 reference should exist", rule2Ref);
+        // rule2 itself should be fully serialized (not circular), but its 
condition's otherRule should be circular
+        Map<String, Object> conditionMap2 = (Map<String, Object>) 
rule2Ref.get("condition");
+        assertNotNull("Rule2's condition should exist", conditionMap2);
+        Map<String, Object> paramValues2 = (Map<String, Object>) 
conditionMap2.get("parameterValues");
+        Map<String, Object> rule1CircularRef = (Map<String, Object>) 
paramValues2.get("otherRule");
+        assertNotNull("Circular reference to rule1 should be detected", 
rule1CircularRef);
+        assertEquals("Should contain circular reference marker", "circular", 
rule1CircularRef.get("$ref"));
+    }
 }
diff --git 
a/tracing/tracing-impl/src/main/java/org/apache/unomi/tracing/impl/DefaultRequestTracer.java
 
b/tracing/tracing-impl/src/main/java/org/apache/unomi/tracing/impl/DefaultRequestTracer.java
index e3c11962b..d15d87b06 100644
--- 
a/tracing/tracing-impl/src/main/java/org/apache/unomi/tracing/impl/DefaultRequestTracer.java
+++ 
b/tracing/tracing-impl/src/main/java/org/apache/unomi/tracing/impl/DefaultRequestTracer.java
@@ -19,7 +19,8 @@ package org.apache.unomi.tracing.impl;
 import org.apache.unomi.tracing.api.RequestTracer;
 import org.apache.unomi.tracing.api.TraceNode;
 
-import java.util.*;
+import java.util.Collection;
+import java.util.Stack;
 
 /**
  * Default implementation of the RequestTracer interface that stores trace 
information in a tree structure
@@ -30,6 +31,24 @@ public class DefaultRequestTracer implements RequestTracer {
     private final ThreadLocal<TraceNode> currentNode = new ThreadLocal<>();
     private final ThreadLocal<TraceNode> rootNode = new ThreadLocal<>();
     private final ThreadLocal<Stack<TraceNode>> nodeStack = 
ThreadLocal.withInitial(Stack::new);
+    private static final int MAX_CONTEXT_STRING_LENGTH = 4096;
+
+    private static String safeContextToString(Object context) {
+        if (context == null) {
+            return "null";
+        }
+        try {
+            String rendered = String.valueOf(context);
+            if (rendered != null && rendered.length() > 
MAX_CONTEXT_STRING_LENGTH) {
+                return rendered.substring(0, MAX_CONTEXT_STRING_LENGTH) + 
"...(truncated)";
+            }
+            return rendered;
+        } catch (StackOverflowError e) {
+            return "<context-toString StackOverflowError class=" + 
context.getClass().getName() + ">";
+        } catch (Throwable t) {
+            return "<context-toString failed class=" + 
context.getClass().getName() + " error=" + t.getClass().getName() + ">";
+        }
+    }
 
     @Override
     public void startOperation(String operationType, String description, 
Object context) {
@@ -42,7 +61,7 @@ public class DefaultRequestTracer implements RequestTracer {
         node.setDescription(description);
         node.setContext(context);
         node.setStartTime(System.currentTimeMillis());
-        
+
         if (rootNode.get() == null) {
             rootNode.set(node);
             currentNode.set(node);
@@ -81,7 +100,7 @@ public class DefaultRequestTracer implements RequestTracer {
         TraceNode node = currentNode.get();
         if (node != null) {
             if (context != null) {
-                node.getTraces().add(message + " - Context: " + context);
+                node.getTraces().add(message + " - Context: " + 
safeContextToString(context));
             } else {
                 node.getTraces().add(message);
             }
@@ -96,7 +115,7 @@ public class DefaultRequestTracer implements RequestTracer {
 
         TraceNode node = currentNode.get();
         if (node != null) {
-            node.getTraces().add("Validation against schema " + schemaId + ": 
" + validationMessages);
+            node.getTraces().add("Validation against schema " + schemaId + ": 
" + safeContextToString(validationMessages));
         }
     }
 
@@ -124,4 +143,4 @@ public class DefaultRequestTracer implements RequestTracer {
         currentNode.remove();
         nodeStack.get().clear();
     }
-} 
\ No newline at end of file
+}
diff --git 
a/tracing/tracing-impl/src/test/java/org/apache/unomi/tracing/impl/DefaultTracerServiceTest.java
 
b/tracing/tracing-impl/src/test/java/org/apache/unomi/tracing/impl/DefaultTracerServiceTest.java
index c36211911..d808fc09d 100644
--- 
a/tracing/tracing-impl/src/test/java/org/apache/unomi/tracing/impl/DefaultTracerServiceTest.java
+++ 
b/tracing/tracing-impl/src/test/java/org/apache/unomi/tracing/impl/DefaultTracerServiceTest.java
@@ -18,9 +18,9 @@ package org.apache.unomi.tracing.impl;
 
 import org.apache.unomi.tracing.api.RequestTracer;
 import org.apache.unomi.tracing.api.TraceNode;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 
 import java.util.Arrays;
 import java.util.concurrent.CountDownLatch;
@@ -28,7 +28,7 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 
 /**
  * Unit tests for {@link DefaultTracerService}
@@ -39,12 +39,12 @@ public class DefaultTracerServiceTest {
     private static final int THREAD_COUNT = 10;
     private static final int TIMEOUT_SECONDS = 10;
 
-    @Before
+    @BeforeEach
     public void setUp() {
         tracerService = new DefaultTracerService();
     }
 
-    @After
+    @AfterEach
     public void tearDown() {
         tracerService.cleanup();
     }
@@ -52,32 +52,32 @@ public class DefaultTracerServiceTest {
     @Test
     public void testGetCurrentTracer() {
         RequestTracer tracer = tracerService.getCurrentTracer();
-        assertNotNull("Current tracer should not be null", tracer);
-        assertTrue("Current tracer should be an instance of 
DefaultRequestTracer", tracer instanceof DefaultRequestTracer);
+        assertNotNull(tracer, "Current tracer should not be null");
+        assertTrue(tracer instanceof DefaultRequestTracer, "Current tracer 
should be an instance of DefaultRequestTracer");
         
         // Test that we get the same tracer instance for the same thread
         RequestTracer secondTracer = tracerService.getCurrentTracer();
-        assertSame("Should get same tracer instance within same thread", 
tracer, secondTracer);
+        assertSame(tracer, secondTracer, "Should get same tracer instance 
within same thread");
     }
 
     @Test
     public void testEnableDisableTracing() {
-        assertFalse("Tracing should be disabled by default", 
tracerService.isTracingEnabled());
+        assertFalse(tracerService.isTracingEnabled(), "Tracing should be 
disabled by default");
 
         tracerService.enableTracing();
-        assertTrue("Tracing should be enabled after enableTracing()", 
tracerService.isTracingEnabled());
+        assertTrue(tracerService.isTracingEnabled(), "Tracing should be 
enabled after enableTracing()");
 
         tracerService.disableTracing();
-        assertFalse("Tracing should be disabled after disableTracing()", 
tracerService.isTracingEnabled());
+        assertFalse(tracerService.isTracingEnabled(), "Tracing should be 
disabled after disableTracing()");
         
         // Verify that enable/disable resets the trace
         tracerService.enableTracing();
         RequestTracer tracer = tracerService.getCurrentTracer();
         tracer.startOperation("test", "description", null);
-        assertNotNull("Should have trace node after operation", 
tracerService.getTraceNode());
+        assertNotNull(tracerService.getTraceNode(), "Should have trace node 
after operation");
         
         tracerService.disableTracing();
-        assertNull("Trace node should be reset after disable", 
tracerService.getTraceNode());
+        assertNull(tracerService.getTraceNode(), "Trace node should be reset 
after disable");
     }
 
     @Test
@@ -104,23 +104,23 @@ public class DefaultTracerServiceTest {
 
         // Get and verify the trace tree
         TraceNode rootNode = tracerService.getTraceNode();
-        assertNotNull("Root node should not be null", rootNode);
-        assertEquals("Root operation type should match", "test", 
rootNode.getOperationType());
-        assertEquals("Root description should match", "Root completed", 
rootNode.getDescription());
-        assertEquals("Root result should match", "root-result", 
rootNode.getResult());
-        assertEquals("Root should have 2 traces", 2, 
rootNode.getTraces().size());
-        assertEquals("Root should have 1 child", 1, 
rootNode.getChildren().size());
-        assertTrue("Root start time should be valid", rootNode.getStartTime() 
>= startTime);
-        assertTrue("Root end time should be valid", rootNode.getEndTime() <= 
endTime);
+        assertNotNull(rootNode, "Root node should not be null");
+        assertEquals("test", rootNode.getOperationType(), "Root operation type 
should match");
+        assertEquals("Root completed", rootNode.getDescription(), "Root 
description should match");
+        assertEquals("root-result", rootNode.getResult(), "Root result should 
match");
+        assertEquals(2, rootNode.getTraces().size(), "Root should have 2 
traces");
+        assertEquals(1, rootNode.getChildren().size(), "Root should have 1 
child");
+        assertTrue(rootNode.getStartTime() >= startTime, "Root start time 
should be valid");
+        assertTrue(rootNode.getEndTime() <= endTime, "Root end time should be 
valid");
 
         TraceNode childNode = rootNode.getChildren().get(0);
-        assertEquals("Child operation type should match", "child", 
childNode.getOperationType());
-        assertEquals("Child description should match", "Child completed", 
childNode.getDescription());
-        assertEquals("Child context should match", "child-context", 
childNode.getContext());
-        assertEquals("Child result should match", "child-result", 
childNode.getResult());
-        assertEquals("Child should have 1 trace", 1, 
childNode.getTraces().size());
-        assertTrue("Child start time should be after root", 
childNode.getStartTime() >= rootNode.getStartTime());
-        assertTrue("Child end time should be before root end", 
childNode.getEndTime() <= rootNode.getEndTime());
+        assertEquals("child", childNode.getOperationType(), "Child operation 
type should match");
+        assertEquals("Child completed", childNode.getDescription(), "Child 
description should match");
+        assertEquals("child-context", childNode.getContext(), "Child context 
should match");
+        assertEquals("child-result", childNode.getResult(), "Child result 
should match");
+        assertEquals(1, childNode.getTraces().size(), "Child should have 1 
trace");
+        assertTrue(childNode.getStartTime() >= rootNode.getStartTime(), "Child 
start time should be after root");
+        assertTrue(childNode.getEndTime() <= rootNode.getEndTime(), "Child end 
time should be before root end");
     }
 
     @Test
@@ -133,12 +133,12 @@ public class DefaultTracerServiceTest {
         tracer.endOperation(false, "Validation failed");
 
         TraceNode node = tracerService.getTraceNode();
-        assertNotNull("Node should not be null", node);
-        assertEquals("Should have 1 validation trace", 1, 
node.getTraces().size());
+        assertNotNull(node, "Node should not be null");
+        assertEquals(1, node.getTraces().size(), "Should have 1 validation 
trace");
         String validationTrace = node.getTraces().get(0);
-        assertTrue("Validation trace should contain schema id", 
validationTrace.contains("test-schema"));
-        assertTrue("Validation trace should contain first error", 
validationTrace.contains("error1"));
-        assertTrue("Validation trace should contain second error", 
validationTrace.contains("error2"));
+        assertTrue(validationTrace.contains("test-schema"), "Validation trace 
should contain schema id");
+        assertTrue(validationTrace.contains("error1"), "Validation trace 
should contain first error");
+        assertTrue(validationTrace.contains("error2"), "Validation trace 
should contain second error");
     }
 
     @Test
@@ -151,7 +151,7 @@ public class DefaultTracerServiceTest {
         tracer.addValidationInfo(Arrays.asList("error"), "schema");
         tracer.endOperation("result", "Completed");
 
-        assertNull("No trace node should be created when tracing is disabled", 
tracerService.getTraceNode());
+        assertNull(tracerService.getTraceNode(), "No trace node should be 
created when tracing is disabled");
     }
 
     @Test
@@ -161,13 +161,13 @@ public class DefaultTracerServiceTest {
 
         tracer.startOperation("test", "Test operation", null);
         tracer.trace("Test message", null);
-        assertNotNull("Should have trace node before reset", 
tracerService.getTraceNode());
+        assertNotNull(tracerService.getTraceNode(), "Should have trace node 
before reset");
 
         tracer.reset();
-        assertNull("Should not have trace node after reset", 
tracerService.getTraceNode());
+        assertNull(tracerService.getTraceNode(), "Should not have trace node 
after reset");
         
         // Verify that tracing is still enabled after reset
-        assertTrue("Tracing should still be enabled after reset", 
tracerService.isTracingEnabled());
+        assertTrue(tracerService.isTracingEnabled(), "Tracing should still be 
enabled after reset");
     }
 
     @Test
@@ -181,9 +181,9 @@ public class DefaultTracerServiceTest {
         tracerService.cleanup();
         RequestTracer newTracer = tracerService.getCurrentTracer();
 
-        assertNotSame("Should get new tracer instance after cleanup", 
originalTracer, newTracer);
-        assertFalse("New tracer should be disabled", newTracer.isEnabled());
-        assertNull("New tracer should have no trace node", 
tracerService.getTraceNode());
+        assertNotSame(originalTracer, newTracer, "Should get new tracer 
instance after cleanup");
+        assertFalse(newTracer.isEnabled(), "New tracer should be disabled");
+        assertNull(tracerService.getTraceNode(), "New tracer should have no 
trace node");
     }
 
     @Test
@@ -210,9 +210,9 @@ public class DefaultTracerServiceTest {
                         
                         // Verify this thread's trace
                         TraceNode node = tracer.getTraceNode();
-                        assertNotNull("Thread " + threadId + " should have a 
trace node", node);
-                        assertEquals("Thread " + threadId + " should have 
correct operation type", 
-                                "thread-" + threadId, node.getOperationType());
+                        assertNotNull(node, "Thread " + threadId + " should 
have a trace node");
+                        assertEquals("thread-" + threadId, 
node.getOperationType(),
+                                "Thread " + threadId + " should have correct 
operation type");
                         
                     } catch (InterruptedException e) {
                         Thread.currentThread().interrupt();
@@ -227,8 +227,8 @@ public class DefaultTracerServiceTest {
             startLatch.countDown();
             
             // Wait for all threads to complete
-            assertTrue("All threads should complete within timeout", 
-                    completionLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS));
+            assertTrue(completionLatch.await(TIMEOUT_SECONDS, 
TimeUnit.SECONDS),
+                    "All threads should complete within timeout");
         } finally {
             executor.shutdown();
             if (!executor.awaitTermination(TIMEOUT_SECONDS, TimeUnit.SECONDS)) 
{
@@ -249,10 +249,33 @@ public class DefaultTracerServiceTest {
         tracer.endOperation(null, null);
 
         TraceNode node = tracerService.getTraceNode();
-        assertNotNull("Node should exist even with null values", node);
-        assertNull("Operation type should be null", node.getOperationType());
-        assertNull("Description should be null", node.getDescription());
-        assertNull("Context should be null", node.getContext());
-        assertNull("Result should be null", node.getResult());
+        assertNotNull(node, "Node should exist even with null values");
+        assertNull(node.getOperationType(), "Operation type should be null");
+        assertNull(node.getDescription(), "Description should be null");
+        assertNull(node.getContext(), "Context should be null");
+        assertNull(node.getResult(), "Result should be null");
+    }
+
+    @Test
+    public void testTraceShouldNotFailWhenContextToStringOverflowsStack() {
+        tracerService.enableTracing();
+        RequestTracer tracer = tracerService.getCurrentTracer();
+
+        tracer.startOperation("test", "Root operation", null);
+        Object badContext = new Object() {
+            @Override
+            public String toString() {
+                return toString();
+            }
+        };
+
+        assertDoesNotThrow(() -> tracer.trace("Test with bad context", 
badContext),
+                "Tracer.trace should not throw even if context.toString 
overflows the stack");
+
+        TraceNode rootNode = tracerService.getTraceNode();
+        assertNotNull(rootNode, "Root node should be created");
+        assertEquals(1, rootNode.getTraces().size(), "Trace should be recorded 
even when context rendering fails");
+        assertTrue(rootNode.getTraces().get(0).contains("StackOverflowError"),
+                "Trace should contain a StackOverflowError marker when context 
rendering overflows");
     }
 } 
\ No newline at end of file


Reply via email to