diff --git a/AyCode.Core/Serializers/AcSerializerCommon.cs b/AyCode.Core/Serializers/AcSerializerCommon.cs
index 29bfb55..5392ad3 100644
--- a/AyCode.Core/Serializers/AcSerializerCommon.cs
+++ b/AyCode.Core/Serializers/AcSerializerCommon.cs
@@ -365,7 +365,7 @@ public static class AcSerializerCommon
/// Resolves a type from its name.
/// Supports AssemblyQualifiedName, FullName, and searches all loaded assemblies as fallback.
///
- private static Type? ResolveTypeName(string typeName)
+ internal static Type? ResolveTypeName(string typeName)
{
// Try direct resolution first (works for AssemblyQualifiedName)
var type = Type.GetType(typeName);
diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
index 53dc7b2..024c7ef 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs
@@ -101,6 +101,7 @@ public static partial class AcBinaryDeserializer
readers[BinaryTypeCode.ObjectWithMetadata] = ReadObjectWithMetadata;
readers[BinaryTypeCode.ObjectWithMetadataRefFirst] = ReadObjectWithMetadataRefFirst;
readers[BinaryTypeCode.ObjectRef] = ReadObjectRef;
+ readers[BinaryTypeCode.ObjectWithTypeName] = ReadObjectWithTypeName;
readers[BinaryTypeCode.Array] = ReadArray;
readers[BinaryTypeCode.Dictionary] = ReadDictionary;
readers[BinaryTypeCode.ByteArray] = static (ctx, _, _) => ReadByteArray(ctx);
@@ -1145,6 +1146,24 @@ public static partial class AcBinaryDeserializer
return ReadObjectCore(context, targetType, depth, cacheIndex: cacheIndex);
}
+ ///
+ /// Polymorphic object prefix: declared property type is System.Object.
+ /// Wire format: [ObjectWithTypeName (68)] [TypeName string] [Object (25) or ObjectRefFirst (66) ...] [props...]
+ /// Reads the runtime type name, resolves it, then delegates to ReadValue with the resolved type
+ /// so the next marker (Object/ObjectRefFirst/etc.) is processed normally.
+ ///
+ private static object? ReadObjectWithTypeName(BinaryDeserializationContext context, Type targetType, int depth)
+ where TInput : struct, IBinaryInputBase
+ {
+ var typeName = ReadPlainString(context);
+ var resolvedType = AcSerializerCommon.ResolveTypeName(typeName)
+ ?? throw new AcBinaryDeserializationException(
+ $"Cannot resolve type '{typeName}' for ObjectWithTypeName at position {context.Position}.",
+ context.Position, null);
+ // Next byte is the actual object marker (Object/ObjectRefFirst/etc.) — read it via ReadValue
+ return ReadValue(context, resolvedType, depth);
+ }
+
///
/// Object olvasás core implementáció.
///
diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
index d600a2f..938b710 100644
--- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
+++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs
@@ -1,18 +1,13 @@
using AyCode.Core.Compression;
-using AyCode.Core.Helpers;
using AyCode.Core.Serializers.Expressions;
using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
-using System.Threading;
using System.Diagnostics;
using System.Linq.Expressions;
-using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
-using AyCode.Core.Serializers.Jsons;
-using static AyCode.Core.Helpers.JsonUtilities;
namespace AyCode.Core.Serializers.Binaries;
@@ -537,7 +532,7 @@ public static partial class AcBinarySerializer
}
var wrapper = context.GetWrapper(type);
- WriteObject(value, wrapper, context, depth, isNested: depth > 0);
+ WriteObject(value, wrapper, context, depth);
}
///
@@ -556,7 +551,7 @@ public static partial class AcBinarySerializer
}
var wrapper = context.GetWrapperBySlot(wrapperSlot, type);
- WriteObject(value, wrapper, context, depth, isNested: depth > 0);
+ WriteObject(value, wrapper, context, depth);
}
#endregion
@@ -626,7 +621,7 @@ public static partial class AcBinarySerializer
}
// Handle complex objects with single-pass reference tracking
- WriteObject(value, wrapper, context, depth, isNested: depth > 0);
+ WriteObject(value, wrapper, context, depth);
}
///
@@ -673,7 +668,7 @@ public static partial class AcBinarySerializer
}
// Handle complex objects with single-pass reference tracking
- WriteObject(value, wrapper, context, depth, isNested: depth > 0);
+ WriteObject(value, wrapper, context, depth);
}
///
@@ -1039,7 +1034,7 @@ public static partial class AcBinarySerializer
#region Complex Type Writers
- private static void WriteObject(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth, bool isNested = false)
+ private static void WriteObject(object value, TypeMetadataWrapper wrapper, BinarySerializationContext context, int depth)
where TOutput : struct, IBinaryOutputBase
{
var metadata = wrapper.Metadata;
@@ -1480,6 +1475,16 @@ public static partial class AcBinarySerializer
else
{
var runtimeType = value.GetType();
+
+ // System.Object declared property → prefix with ObjectWithTypeName marker + TypeName
+ // so the deserializer can resolve the concrete runtime type.
+ // The normal Object/ObjectRefFirst marker follows as usual.
+ if (prop.IsObjectDeclaredType && !context.UseMetadata)
+ {
+ context.WriteByte(BinaryTypeCode.ObjectWithTypeName);
+ context.WriteStringUtf8(runtimeType.AssemblyQualifiedName!);
+ }
+
var complexIdx = prop.ComplexPropertyIndex;
if (complexIdx >= 0)
{
diff --git a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs
index 1588b51..a25bfd9 100644
--- a/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs
+++ b/AyCode.Core/Serializers/Binaries/BinaryPropertyAccessorBase.cs
@@ -27,11 +27,18 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
public int ComplexPropertyIndex { get; internal set; } = -1;
public bool IsStringCollectionProperty { get; }
+ ///
+ /// True when declared property type is System.Object.
+ /// Used to trigger ObjectWithTypeName marker (68) so the deserializer
+ /// can resolve the concrete runtime type.
+ ///
+ public bool IsObjectDeclaredType { get; }
+
///
/// Cached [AcStringIntern] attribute value for this property.
/// null = no attribute (follow global StringInterningMode)
- /// true = [AcStringIntern(true)] — force intern
- /// false = [AcStringIntern(false)] — force skip
+ /// true = [AcStringIntern(true)] � force intern
+ /// false = [AcStringIntern(false)] � force skip
///
private readonly byte _interningFlags;
@@ -58,6 +65,7 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
: base(prop, declaringType)
{
IsStringCollectionProperty = IsStringCollection(prop.PropertyType);
+ IsObjectDeclaredType = prop.PropertyType == typeof(object);
// All typed getters are initialized in PropertyAccessorBase
if (enableInternString && (AccessorType == PropertyAccessorType.String || IsStringCollectionProperty))
@@ -113,6 +121,6 @@ public abstract class BinaryPropertyAccessorBase : PropertyAccessorBase
PropertyAccessorType.UInt64 => BinaryTypeCode.UInt64,
PropertyAccessorType.Boolean => BinaryTypeCode.True,
PropertyAccessorType.Enum => BinaryTypeCode.Enum,
- _ => null // String, Object — always write marker to stream
+ _ => null // String, Object � always write marker to stream
};
}
diff --git a/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs b/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs
index 6ea8313..7514f5c 100644
--- a/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs
+++ b/AyCode.Core/Serializers/Binaries/BinaryTypeCode.cs
@@ -55,6 +55,14 @@ internal static class BinaryTypeCode
public const byte ObjectRefFirst = 66; // First occurrence of tracked object (ref handling enabled)
public const byte ObjectWithMetadataRefFirst = 67; // First occurrence of tracked object with metadata
+ // Polymorphic object markers (68-69): self-describing object for polymorphic properties.
+ // Used when declared property type ≠runtime type AND UseMetadata=false.
+ // Serializer writes runtime type name inline so deserializer can resolve the concrete type.
+ // Format: [ObjectWithTypeName (68)] [VarUInt typeNameLen] [UTF8 typeName] [properties...] [ObjectEnd]
+ // Format: [ObjectWithTypeNameRefFirst (69)] [VarUInt cacheIndex] [VarUInt typeNameLen] [UTF8 typeName] [properties...] [ObjectEnd]
+ public const byte ObjectWithTypeName = 68;
+ public const byte ObjectWithTypeNameRefFirst = 69;
+
// Special markers (32+, for header/meta)
// Header flags byte structure (for values >= 64):
// Bit 0 (0x01): HasMetadata