diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 8866d3a..5ced01b 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -24,7 +24,8 @@
"Bash(dir /B /O-D \"H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.md\")",
"Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.md\")",
"Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log 2>nul | head -3\")",
- "Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log 2>nul | head -1\")"
+ "Bash(cmd /c \"dir /B /O-D H:\\Applications\\Aycode\\Source\\AyCode.Core\\Test_Benchmark_Results\\Benchmark\\*.log 2>nul | head -1\")",
+ "Bash(timeout 30 dotnet run:*)"
]
}
}
diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs
index 8ed59d2..ad5cdec 100644
--- a/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs
+++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.MetaSection.cs
@@ -106,7 +106,8 @@ public static partial class AcToonSerializer
}
///
- /// Collect all types that need documentation (recursive).
+ /// Recursively collects all types that need metadata documentation.
+ /// Uses the same logic as AcBinarySerializer.RegisterMetadataForType.
///
private static void CollectTypes(Type type, HashSet types)
{
@@ -116,15 +117,7 @@ public static partial class AcToonSerializer
if (!types.Add(underlyingType)) return; // Already processed
- // Handle collections
- var elementType = GetCollectionElementType(underlyingType);
- if (elementType != null)
- {
- CollectTypes(elementType, types);
- return;
- }
-
- // Handle dictionaries
+ // Handle dictionaries FIRST (before generic IEnumerable check)
if (IsDictionaryType(underlyingType, out var keyType, out var valueType))
{
if (keyType != null) CollectTypes(keyType, types);
@@ -132,7 +125,18 @@ public static partial class AcToonSerializer
return;
}
- // Handle object properties
+ // Handle collections/arrays (matches AcBinarySerializer logic)
+ if (typeof(IEnumerable).IsAssignableFrom(underlyingType) && !ReferenceEquals(underlyingType, StringType))
+ {
+ var elementType = GetCollectionElementType(underlyingType);
+ if (elementType != null)
+ {
+ CollectTypes(elementType, types);
+ }
+ return;
+ }
+
+ // Handle object properties (traverse type graph)
var metadata = GetTypeMetadata(underlyingType);
foreach (var prop in metadata.Properties)
{
@@ -395,8 +399,11 @@ public static partial class AcToonSerializer
return $"Enum type with values: {string.Join(", ", Enum.GetNames(type))}";
var metadata = GetTypeMetadata(type);
- if (metadata.IsCollection)
- return $"Collection of {metadata.ElementType?.Name ?? "items"}";
+
+ // Only treat as collection if ElementType is not System.Object (fallback value from GetCollectionElementType)
+ if (metadata.IsCollection && metadata.ElementType != null && metadata.ElementType != typeof(object))
+ return $"Collection of {metadata.ElementType.Name}";
+
if (metadata.IsDictionary)
return "Dictionary mapping keys to values";
diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs
index 8dd49b9..ae6420d 100644
--- a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs
+++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonSerializationContext.cs
@@ -4,7 +4,7 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
-using ReferenceEqualityComparer = AyCode.Core.Serializers.Jsons.ReferenceEqualityComparer;
+using ReferenceEqualityComparer = AyCode.Core.Serializers.ReferenceEqualityComparer;
namespace AyCode.Core.Serializers.Toons;
diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonTypeMetadata.cs b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonTypeMetadata.cs
index fcd2c07..c6f365a 100644
--- a/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonTypeMetadata.cs
+++ b/AyCode.Core/Serializers/Toons/AcToonSerializer.ToonTypeMetadata.cs
@@ -1,3 +1,4 @@
+using System.Collections;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
@@ -28,12 +29,16 @@ public static partial class AcToonSerializer
// Get custom attribute if present
CustomDescription = type.GetCustomAttribute();
- // Check if collection or dictionary
+ // Check if collection or dictionary (matches AcBinarySerializer logic)
IsDictionary = IsDictionaryType(type, out _, out _);
if (!IsDictionary)
{
- ElementType = GetCollectionElementType(type);
- IsCollection = ElementType != null;
+ // Only treat as collection if it implements IEnumerable (excluding string)
+ if (typeof(IEnumerable).IsAssignableFrom(type) && !ReferenceEquals(type, StringType))
+ {
+ ElementType = GetCollectionElementType(type);
+ IsCollection = true;
+ }
}
// Build property accessors
@@ -137,8 +142,13 @@ public static partial class AcToonSerializer
return false;
}
+ // Thread-local visited set to prevent circular reference stack overflow
+ [ThreadStatic]
+ private static HashSet? t_visitedTypes;
+
///
/// Build human-readable type name for meta section.
+ /// Uses thread-local visited set to prevent stack overflow on circular references.
///
private static string BuildTypeDisplayName(Type type)
{
@@ -169,30 +179,60 @@ public static partial class AcToonSerializer
return isNullable ? baseName + "?" : baseName;
}
+ ///
+ /// Get complex type name WITH circular reference protection using thread-local visited set.
+ ///
private static string GetComplexTypeName(Type type)
{
- if (ReferenceEquals(type, GuidType)) return "guid";
- if (ReferenceEquals(type, DateTimeOffsetType)) return "datetimeoffset";
- if (ReferenceEquals(type, TimeSpanType)) return "timespan";
- if (type.IsEnum) return "enum";
+ // Initialize thread-local visited set if needed
+ t_visitedTypes ??= new HashSet();
- // Check for collections
- var elementType = GetCollectionElementType(type);
- if (elementType != null)
+ // Check for circular reference - if already visiting, return simple name
+ if (!t_visitedTypes.Add(type))
{
- var elementTypeName = BuildTypeDisplayName(elementType);
- return $"{elementTypeName}[]";
+ return type.Name; // Break circular reference
}
- // Check for dictionaries
- if (IsDictionaryType(type, out var keyType, out var valueType))
+ try
{
- var keyTypeName = keyType != null ? BuildTypeDisplayName(keyType) : "object";
- var valueTypeName = valueType != null ? BuildTypeDisplayName(valueType) : "object";
- return $"dict<{keyTypeName}, {valueTypeName}>";
- }
+ if (ReferenceEquals(type, GuidType)) return "guid";
+ if (ReferenceEquals(type, DateTimeOffsetType)) return "datetimeoffset";
+ if (ReferenceEquals(type, TimeSpanType)) return "timespan";
+ if (type.IsEnum) return "enum";
- return type.Name;
+ // For collections: recursively build element type name (safe with visited tracking)
+ if (typeof(IEnumerable).IsAssignableFrom(type) && !ReferenceEquals(type, StringType))
+ {
+ var elementType = GetCollectionElementType(type);
+ if (elementType != null && elementType != typeof(object))
+ {
+ var elementTypeName = BuildTypeDisplayName(elementType);
+ return $"{elementTypeName}[]";
+ }
+ }
+
+ // For dictionaries: recursively build key/value type names (safe with visited tracking)
+ if (IsDictionaryType(type, out var keyType, out var valueType))
+ {
+ var keyTypeName = keyType != null ? BuildTypeDisplayName(keyType) : "object";
+ var valueTypeName = valueType != null ? BuildTypeDisplayName(valueType) : "object";
+ return $"dict<{keyTypeName}, {valueTypeName}>";
+ }
+
+ // Simple type name for complex objects
+ return type.Name;
+ }
+ finally
+ {
+ // Remove from visited set when done (allows reuse in different branches)
+ t_visitedTypes.Remove(type);
+
+ // Clear the set if empty to avoid memory leaks
+ if (t_visitedTypes.Count == 0)
+ {
+ t_visitedTypes = null;
+ }
+ }
}
}
}
diff --git a/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs b/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs
index 23f0567..0f75579 100644
--- a/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs
+++ b/AyCode.Core/Serializers/Toons/AcToonSerializerOptions.cs
@@ -181,7 +181,7 @@ public sealed class AcToonSerializerOptions : AcSerializerOptions
{
Mode = ToonSerializationMode.DataOnly,
UseMeta = false,
- UseIndentation = false,
+ UseIndentation = true,
OmitDefaultValues = true,
WriteTypeNames = false,
UseReferenceHandling = false