diff --git a/AyCode.Core/Extensions/AcJsonDeserializer.cs b/AyCode.Core/Extensions/AcJsonDeserializer.cs
index a502b45..d0bf33a 100644
--- a/AyCode.Core/Extensions/AcJsonDeserializer.cs
+++ b/AyCode.Core/Extensions/AcJsonDeserializer.cs
@@ -1,9 +1,12 @@
+using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
+using System.Collections.Frozen;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Text;
using System.Text.Json;
using AyCode.Core.Helpers;
using AyCode.Core.Interfaces;
@@ -30,23 +33,28 @@ public class AcJsonDeserializationException : Exception
///
/// High-performance custom JSON deserializer optimized for IId<T> reference handling.
-/// Supports MaxDepth and UseReferenceHandling options.
+/// Uses Utf8JsonReader for streaming deserialization when possible (STJ approach).
///
public static class AcJsonDeserializer
{
private static readonly ConcurrentDictionary TypeMetadataCache = new();
+
+ // Pre-computed JSON property names for fast lookup (UTF-8 bytes)
+ private static readonly byte[] RefPropertyUtf8 = "$ref"u8.ToArray();
+ private static readonly byte[] IdPropertyUtf8 = "$id"u8.ToArray();
#region Public API
///
/// Deserialize JSON string to a new object of type T with default options.
///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T? Deserialize(string json) => Deserialize(json, AcJsonSerializerOptions.Default);
///
/// Deserialize JSON string to a new object of type T with specified options.
///
- public static T? Deserialize(string json, AcJsonSerializerOptions options)
+ public static T? Deserialize(string json, in AcJsonSerializerOptions options)
{
if (string.IsNullOrEmpty(json) || json == "null") return default;
@@ -54,18 +62,30 @@ public static class AcJsonDeserializer
try
{
- ValidateJson(json, targetType);
-
- if (TryDeserializePrimitive(json, targetType, out var primitiveResult))
+ if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult))
return (T?)primitiveResult;
- var context = new DeserializationContext(options);
+ ValidateJson(json, targetType);
+
+ // Fast path for no reference handling - use Utf8JsonReader (streaming, no DOM)
+ if (!options.UseReferenceHandling)
+ {
+ return DeserializeWithUtf8Reader(json, options.MaxDepth);
+ }
+
+ // Reference handling requires DOM for forward references
using var doc = JsonDocument.Parse(json);
-
- var result = ReadValue(doc.RootElement, targetType, context, 0);
- context.ResolveReferences();
-
- return (T?)result;
+ var context = DeserializationContextPool.Get(options);
+ try
+ {
+ var result = ReadValue(doc.RootElement, targetType, context, 0);
+ context.ResolveReferences();
+ return (T?)result;
+ }
+ finally
+ {
+ DeserializationContextPool.Return(context);
+ }
}
catch (AcJsonDeserializationException) { throw; }
catch (System.Text.Json.JsonException ex)
@@ -81,37 +101,48 @@ public static class AcJsonDeserializer
///
/// Deserialize JSON string to specified type with default options.
///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static object? Deserialize(string json, Type targetType) => Deserialize(json, targetType, AcJsonSerializerOptions.Default);
///
/// Deserialize JSON string to specified type with specified options.
///
- public static object? Deserialize(string json, Type targetType, AcJsonSerializerOptions options)
+ public static object? Deserialize(string json, in Type targetType, in AcJsonSerializerOptions options)
{
if (string.IsNullOrEmpty(json) || json == "null") return null;
try
{
- ValidateJson(json, targetType);
+ var firstChar = json[0];
+ var isArrayJson = firstChar == '[';
+ var isObjectJson = firstChar == '{';
- var isArrayJson = json.Length > 0 && json[0] == '[';
- var isObjectJson = json.Length > 0 && json[0] == '{';
- var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType);
- var isDictType = IsDictionaryType(targetType, out _, out _);
-
- if (!isArrayJson && !isCollectionType && !(isObjectJson && isDictType))
+ if (!isArrayJson && !isObjectJson)
{
- if (TryDeserializePrimitive(json, targetType, out var primitiveResult))
+ if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult))
return primitiveResult;
}
- var context = new DeserializationContext(options);
+ ValidateJson(json, targetType);
+
+ // Fast path for no reference handling
+ if (!options.UseReferenceHandling)
+ {
+ return DeserializeWithUtf8ReaderNonGeneric(json, targetType, options.MaxDepth);
+ }
+
using var doc = JsonDocument.Parse(json);
-
- var result = ReadValue(doc.RootElement, targetType, context, 0);
- context.ResolveReferences();
-
- return result;
+ var context = DeserializationContextPool.Get(options);
+ try
+ {
+ var result = ReadValue(doc.RootElement, targetType, context, 0);
+ context.ResolveReferences();
+ return result;
+ }
+ finally
+ {
+ DeserializationContextPool.Return(context);
+ }
}
catch (AcJsonDeserializationException) { throw; }
catch (System.Text.Json.JsonException ex)
@@ -127,74 +158,115 @@ public static class AcJsonDeserializer
///
/// Populate existing object with JSON data (merge mode) with default options.
///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate(string json, T target) where T : class
=> Populate(json, target, AcJsonSerializerOptions.Default);
///
/// Populate existing object with JSON data (merge mode) with specified options.
///
- public static void Populate(string json, T target, AcJsonSerializerOptions options) where T : class
+ public static void Populate(string json, T target, in AcJsonSerializerOptions options) where T : class
{
ArgumentNullException.ThrowIfNull(target);
if (string.IsNullOrEmpty(json) || json == "null") return;
- Populate(json, (object)target, target.GetType(), options);
+ PopulateInternal(json, target, target.GetType(), options);
}
///
/// Populate existing object with JSON data with default options.
///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate(string json, object target)
=> Populate(json, target, target.GetType(), AcJsonSerializerOptions.Default);
///
/// Populate existing object with JSON data with specified options.
///
- public static void Populate(string json, object target, AcJsonSerializerOptions options)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Populate(string json, object target, in AcJsonSerializerOptions options)
=> Populate(json, target, target.GetType(), options);
///
/// Populate existing object with JSON data with default options.
///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate(string json, object target, Type targetType)
=> Populate(json, target, targetType, AcJsonSerializerOptions.Default);
///
/// Populate existing object with JSON data with specified options.
///
- public static void Populate(string json, object target, Type targetType, AcJsonSerializerOptions options)
+ public static void Populate(string json, object target, in Type targetType, in AcJsonSerializerOptions options)
{
ArgumentNullException.ThrowIfNull(target);
if (string.IsNullOrEmpty(json) || json == "null") return;
-
+ PopulateInternal(json, target, targetType, options);
+ }
+
+ private static void PopulateInternal(string json, object target, in Type targetType, in AcJsonSerializerOptions options)
+ {
try
{
ValidateJson(json, targetType);
- var context = new DeserializationContext(options) { IsMergeMode = true };
- using var doc = JsonDocument.Parse(json);
+ // Fast path for no reference handling - use Utf8JsonReader streaming
+ if (!options.UseReferenceHandling)
+ {
+ var firstChar = json[0];
+
+ if (firstChar == '[')
+ {
+ if (target is IList targetList)
+ PopulateListWithUtf8Reader(json, targetList, targetType, options.MaxDepth);
+ else
+ throw new AcJsonDeserializationException($"Cannot populate non-list target '{targetType.Name}' with JSON array", json, targetType);
+ return;
+ }
+
+ if (firstChar == '{')
+ {
+ var metadata = GetTypeMetadata(targetType);
+ PopulateObjectWithUtf8Reader(json, target, metadata, options.MaxDepth);
+ return;
+ }
+
+ throw new AcJsonDeserializationException($"Cannot populate object with JSON starting with '{firstChar}'", json, targetType);
+ }
+ // Reference handling requires DOM for forward references
+ using var doc = JsonDocument.Parse(json);
var rootElement = doc.RootElement;
- if (rootElement.ValueKind == JsonValueKind.Array)
+ var context = DeserializationContextPool.Get(options);
+ context.IsMergeMode = true;
+
+ try
{
- if (target is IList targetList)
- PopulateList(rootElement, targetList, targetType, context, 0);
+ if (rootElement.ValueKind == JsonValueKind.Array)
+ {
+ if (target is IList targetList)
+ PopulateList(rootElement, targetList, targetType, context, 0);
+ else
+ throw new AcJsonDeserializationException($"Cannot populate non-list target '{targetType.Name}' with JSON array", json, targetType);
+
+ context.ResolveReferences();
+ return;
+ }
+
+ if (rootElement.ValueKind == JsonValueKind.Object)
+ {
+ var metadata = GetTypeMetadata(targetType);
+ PopulateObjectInternalMerge(rootElement, target, metadata, context, 0);
+ }
else
- throw new AcJsonDeserializationException($"Cannot populate non-list target '{targetType.Name}' with JSON array", json, targetType);
+ throw new AcJsonDeserializationException($"Cannot populate object with JSON value of kind '{rootElement.ValueKind}'", json, targetType);
context.ResolveReferences();
- return;
}
-
- if (rootElement.ValueKind == JsonValueKind.Object)
+ finally
{
- var metadata = GetTypeMetadata(targetType);
- PopulateObjectInternalMerge(rootElement, target, metadata, context, 0);
+ DeserializationContextPool.Return(context);
}
- else
- throw new AcJsonDeserializationException($"Cannot populate object with JSON value of kind '{rootElement.ValueKind}'", json, targetType);
-
- context.ResolveReferences();
}
catch (AcJsonDeserializationException) { throw; }
catch (System.Text.Json.JsonException ex)
@@ -209,71 +281,202 @@ public static class AcJsonDeserializer
#endregion
- #region Validation
+ #region Utf8JsonReader Fast Path (STJ-style streaming)
- private static void ValidateJson(string json, Type targetType)
+ ///
+ /// Deserialize using Utf8JsonReader - streaming without DOM allocation.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static T? DeserializeWithUtf8Reader(string json, byte maxDepth)
{
- if (string.IsNullOrEmpty(json)) return;
-
- if (json.Length > 2 && json[0] == '"' && json[^1] == '"')
+ var (buffer, length) = GetUtf8Bytes(json);
+ try
{
- var inner = json[1..^1];
- if (inner.Contains("\\\"") && (inner.Contains("{") || inner.Contains("[")))
- throw new AcJsonDeserializationException(
- $"Detected double-serialized JSON string. Target type: {targetType.Name}.",
- json, targetType);
+ var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
+ if (!reader.Read()) return default;
+ return (T?)ReadValueFromReader(ref reader, typeof(T), maxDepth, 0);
+ }
+ finally
+ {
+ ReturnUtf8Buffer(buffer);
}
-
- var isArrayJson = json.Length > 0 && json[0] == '[';
- var isObjectJson = json.Length > 0 && json[0] == '{';
- var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType);
- var isDictType = IsDictionaryType(targetType, out _, out _);
-
- if (isArrayJson && !isCollectionType && !isDictType && targetType != typeof(object))
- throw new AcJsonDeserializationException($"JSON is an array but target type '{targetType.Name}' is not a collection type.", json, targetType);
-
- if (isObjectJson && isCollectionType && !isDictType)
- throw new AcJsonDeserializationException($"JSON is an object but target type '{targetType.Name}' is a collection type (not dictionary).", json, targetType);
}
- #endregion
-
- #region Core Reading Methods
-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static object? ReadValue(JsonElement element, Type targetType, DeserializationContext context, int depth)
+ private static object? DeserializeWithUtf8ReaderNonGeneric(string json, Type targetType, byte maxDepth)
{
- return element.ValueKind switch
+ var (buffer, length) = GetUtf8Bytes(json);
+ try
{
- JsonValueKind.Object => ReadObject(element, targetType, context, depth),
- JsonValueKind.Array => ReadArray(element, targetType, context, depth),
- JsonValueKind.Null => null,
- _ => ReadPrimitive(element, targetType, element.ValueKind)
+ var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
+ if (!reader.Read()) return null;
+ return ReadValueFromReader(ref reader, targetType, maxDepth, 0);
+ }
+ finally
+ {
+ ReturnUtf8Buffer(buffer);
+ }
+ }
+
+ private static object? ReadValueFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth)
+ {
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.Null:
+ return null;
+ case JsonTokenType.True:
+ return true;
+ case JsonTokenType.False:
+ return false;
+ case JsonTokenType.Number:
+ return ReadNumberFromReader(ref reader, targetType);
+ case JsonTokenType.String:
+ return ReadStringFromReader(ref reader, targetType);
+ case JsonTokenType.StartObject:
+ return ReadObjectFromReader(ref reader, targetType, maxDepth, depth);
+ case JsonTokenType.StartArray:
+ return ReadArrayFromReader(ref reader, targetType, maxDepth, depth);
+ default:
+ return null;
+ }
+ }
+
+ ///
+ /// Read number value using cached PropertySetterInfo (avoids Nullable.GetUnderlyingType + Type.GetTypeCode calls).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static object? ReadNumberFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo)
+ {
+ return propInfo.PropertyTypeCode switch
+ {
+ TypeCode.Int32 => reader.GetInt32(),
+ TypeCode.Int64 => reader.GetInt64(),
+ TypeCode.Double => reader.GetDouble(),
+ TypeCode.Decimal => reader.GetDecimal(),
+ TypeCode.Single => reader.GetSingle(),
+ TypeCode.Byte => reader.GetByte(),
+ TypeCode.Int16 => reader.GetInt16(),
+ TypeCode.UInt16 => reader.GetUInt16(),
+ TypeCode.UInt32 => reader.GetUInt32(),
+ TypeCode.UInt64 => reader.GetUInt64(),
+ TypeCode.SByte => reader.GetSByte(),
+ _ => propInfo.UnderlyingType.IsEnum ? Enum.ToObject(propInfo.UnderlyingType, reader.GetInt32()) : reader.GetDouble()
+ };
+ }
+
+ ///
+ /// Read string value using cached PropertySetterInfo (avoids Nullable.GetUnderlyingType calls).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static object? ReadStringFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo)
+ {
+ var type = propInfo.UnderlyingType;
+
+ if (ReferenceEquals(type, StringType)) return reader.GetString();
+ if (ReferenceEquals(type, DateTimeType)) return reader.GetDateTime();
+ if (ReferenceEquals(type, GuidType)) return reader.GetGuid();
+ if (ReferenceEquals(type, DateTimeOffsetType)) return reader.GetDateTimeOffset();
+ if (ReferenceEquals(type, TimeSpanType))
+ {
+ var str = reader.GetString();
+ return str != null ? TimeSpan.Parse(str, CultureInfo.InvariantCulture) : default(TimeSpan);
+ }
+ if (type.IsEnum)
+ {
+ var str = reader.GetString();
+ return str != null ? Enum.Parse(type, str) : null;
+ }
+
+ return reader.GetString();
+ }
+
+ ///
+ /// Read value from reader using cached PropertySetterInfo for faster type resolution.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static object? ReadValueFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo, byte maxDepth, int depth)
+ {
+ switch (reader.TokenType)
+ {
+ case JsonTokenType.Null:
+ return null;
+ case JsonTokenType.True:
+ return true;
+ case JsonTokenType.False:
+ return false;
+ case JsonTokenType.Number:
+ return ReadNumberFromReaderCached(ref reader, propInfo);
+ case JsonTokenType.String:
+ return ReadStringFromReaderCached(ref reader, propInfo);
+ case JsonTokenType.StartObject:
+ return ReadObjectFromReader(ref reader, propInfo.PropertyType, maxDepth, depth);
+ case JsonTokenType.StartArray:
+ return ReadArrayFromReader(ref reader, propInfo.PropertyType, maxDepth, depth);
+ default:
+ return null;
+ }
+ }
+
+ private static object? ReadNumberFromReader(ref Utf8JsonReader reader, Type targetType)
+ {
+ var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
+ var typeCode = Type.GetTypeCode(type);
+
+ return typeCode switch
+ {
+ TypeCode.Int32 => reader.GetInt32(),
+ TypeCode.Int64 => reader.GetInt64(),
+ TypeCode.Double => reader.GetDouble(),
+ TypeCode.Decimal => reader.GetDecimal(),
+ TypeCode.Single => reader.GetSingle(),
+ TypeCode.Byte => reader.GetByte(),
+ TypeCode.Int16 => reader.GetInt16(),
+ TypeCode.UInt16 => reader.GetUInt16(),
+ TypeCode.UInt32 => reader.GetUInt32(),
+ TypeCode.UInt64 => reader.GetUInt64(),
+ TypeCode.SByte => reader.GetSByte(),
+ _ => type.IsEnum ? Enum.ToObject(type, reader.GetInt32()) : reader.GetDouble()
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static object? ReadObject(JsonElement element, Type targetType, DeserializationContext context, int depth)
+ private static object? ReadStringFromReader(ref Utf8JsonReader reader, Type targetType)
{
- // Handle $ref even if reference handling is disabled (for compatibility)
- if (context.UseReferenceHandling && element.TryGetProperty("$ref", out var refElement))
+ var type = Nullable.GetUnderlyingType(targetType) ?? targetType;
+
+ if (ReferenceEquals(type, StringType)) return reader.GetString();
+ if (ReferenceEquals(type, DateTimeType)) return reader.GetDateTime();
+ if (ReferenceEquals(type, GuidType)) return reader.GetGuid();
+ if (ReferenceEquals(type, DateTimeOffsetType)) return reader.GetDateTimeOffset();
+ if (ReferenceEquals(type, TimeSpanType))
{
- var refId = refElement.GetString()!;
- return context.TryGetReferencedObject(refId, out var refObj) ? refObj : new DeferredReference(refId, targetType);
+ var str = reader.GetString();
+ return str != null ? TimeSpan.Parse(str, CultureInfo.InvariantCulture) : default(TimeSpan);
+ }
+ if (type.IsEnum)
+ {
+ var str = reader.GetString();
+ return str != null ? Enum.Parse(type, str) : null;
+ }
+
+ return reader.GetString();
+ }
+
+ private static object? ReadObjectFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth)
+ {
+ if (depth > maxDepth)
+ {
+ reader.Skip();
+ return null;
}
- // Check depth limit
- if (depth > context.MaxDepth) return null;
-
if (IsDictionaryType(targetType, out var keyType, out var valueType))
- return ReadDictionary(element, keyType!, valueType!, context, depth);
+ return ReadDictionaryFromReader(ref reader, keyType!, valueType!, maxDepth, depth);
var metadata = GetTypeMetadata(targetType);
- object? instance;
- if (metadata.CompiledConstructor != null)
- instance = metadata.CompiledConstructor.Invoke();
- else
+ var instance = metadata.CompiledConstructor?.Invoke();
+ if (instance == null)
{
try { instance = Activator.CreateInstance(targetType); }
catch (MissingMethodException ex)
@@ -286,172 +489,310 @@ public static class AcJsonDeserializer
if (instance == null) return null;
- if (context.UseReferenceHandling && element.TryGetProperty("$id", out var idElement))
- context.RegisterObject(idElement.GetString()!, instance);
-
- PopulateObjectInternal(element, instance, metadata, context, depth);
+ var propsDict = metadata.PropertySettersFrozen;
+ var nextDepth = depth + 1;
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndObject)
+ break;
+
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ continue;
+
+ var propName = reader.GetString();
+ if (propName == null || !reader.Read())
+ continue;
+
+ if (!propsDict.TryGetValue(propName, out var propInfo))
+ {
+ reader.Skip();
+ continue;
+ }
+
+ // Use cached version for faster type resolution
+ var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth);
+ propInfo.SetValue(instance, value);
+ }
return instance;
}
- private static void PopulateObjectInternal(JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth)
+ ///
+ /// Read object from reader when we're already at StartObject.
+ ///
+ private static object? ReadObjectFromReaderAtStart(ref Utf8JsonReader reader, Type targetType, DeserializeTypeMetadata metadata, byte maxDepth, int depth)
{
- foreach (var jsonProp in element.EnumerateObject())
+ if (depth > maxDepth)
{
- var propName = jsonProp.Name;
-
- if (propName.Length > 0 && propName[0] == '$' && (propName == "$id" || propName == "$ref"))
- continue;
-
- if (!metadata.PropertySetters.TryGetValue(propName, out var propInfo)) continue;
-
- var value = ReadValue(jsonProp.Value, propInfo.PropertyType, context, depth + 1);
-
- if (value is DeferredReference deferred)
- context.AddPropertyToResolve(target, propInfo, deferred.RefId);
- else
- propInfo.SetValue(target, value);
+ reader.Skip();
+ return null;
}
- }
- private static void PopulateObjectInternalMerge(JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth)
- {
- foreach (var jsonProp in element.EnumerateObject())
+ var instance = metadata.CompiledConstructor?.Invoke();
+ if (instance == null)
{
- var propName = jsonProp.Name;
-
- if (propName.Length > 0 && propName[0] == '$' && (propName == "$id" || propName == "$ref"))
- continue;
-
- if (!metadata.PropertySetters.TryGetValue(propName, out var propInfo)) continue;
-
- var propValue = jsonProp.Value;
- var propValueKind = propValue.ValueKind;
-
- // Check depth limit for nested objects/collections
- if (depth + 1 > context.MaxDepth)
+ try { instance = Activator.CreateInstance(targetType); }
+ catch (MissingMethodException ex)
{
- // At max depth, only set primitives
- if (propValueKind != JsonValueKind.Object && propValueKind != JsonValueKind.Array)
- {
- var primitiveValue = ReadPrimitive(propValue, propInfo.PropertyType, propValueKind);
- propInfo.SetValue(target, primitiveValue);
- }
- continue;
+ throw new AcJsonDeserializationException(
+ $"Cannot deserialize type '{targetType.FullName}' because it does not have a parameterless constructor.",
+ null, targetType, ex);
}
-
- if (propInfo is { IsCollection: true, ElementIsIId: true } && propValueKind == JsonValueKind.Array)
- {
- var existingCollection = propInfo.GetValue(target);
- if (existingCollection != null)
- {
- MergeIIdCollection(propValue, existingCollection, propInfo, context, depth);
- continue;
- }
- }
-
- if (propValueKind == JsonValueKind.Object)
- {
- if (context.UseReferenceHandling && propValue.TryGetProperty("$ref", out _))
- {
- var value = ReadValue(propValue, propInfo.PropertyType, context, depth + 1);
- if (value is DeferredReference deferred)
- context.AddPropertyToResolve(target, propInfo, deferred.RefId);
- else
- propInfo.SetValue(target, value);
- continue;
- }
-
- if (!propInfo.PropertyType.IsPrimitive && propInfo.PropertyType != StringType)
- {
- var existingObj = propInfo.GetValue(target);
- if (existingObj != null)
- {
- var nestedMetadata = GetTypeMetadata(propInfo.PropertyType);
- PopulateObjectInternalMerge(propValue, existingObj, nestedMetadata, context, depth + 1);
- continue;
- }
- }
- }
-
- var value2 = ReadValue(propValue, propInfo.PropertyType, context, depth + 1);
-
- if (value2 is DeferredReference deferred2)
- context.AddPropertyToResolve(target, propInfo, deferred2.RefId);
- else
- propInfo.SetValue(target, value2);
}
- }
-
- private static void PopulateList(JsonElement arrayElement, IList targetList, Type listType, DeserializationContext context, int depth)
- {
- // Check depth limit
- if (depth > context.MaxDepth) return;
- var elementType = GetCollectionElementType(listType);
- if (elementType == null) return;
-
- var acObservable = targetList as IAcObservableCollection;
- acObservable?.BeginUpdate();
+ if (instance == null) return null;
- try
+ var propsDict = metadata.PropertySettersFrozen;
+ var nextDepth = depth + 1;
+
+ while (reader.Read())
{
- targetList.Clear();
+ if (reader.TokenType == JsonTokenType.EndObject)
+ break;
- foreach (var item in arrayElement.EnumerateArray())
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ continue;
+
+ var propName = reader.GetString();
+ if (propName == null || !reader.Read())
+ continue;
+
+ if (!propsDict.TryGetValue(propName, out var propInfo))
{
- var value = ReadValue(item, elementType, context, depth + 1);
- if (value != null)
- targetList.Add(value);
+ reader.Skip();
+ continue;
}
+
+ // Use cached version for faster type resolution
+ var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth);
+ propInfo.SetValue(instance, value);
}
- finally { acObservable?.EndUpdate(); }
+
+ return instance;
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static object? ReadArray(JsonElement element, Type targetType, DeserializationContext context, int depth)
+ private static object? ReadArrayFromReader(ref Utf8JsonReader reader, Type targetType, byte maxDepth, int depth)
{
- // Check depth limit
- if (depth > context.MaxDepth) return null;
+ if (depth > maxDepth)
+ {
+ reader.Skip();
+ return null;
+ }
var elementType = GetCollectionElementType(targetType);
if (elementType == null) return null;
+ var nextDepth = depth + 1;
+ var list = GetOrCreateListFactory(elementType)();
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndArray)
+ break;
+
+ var value = ReadValueFromReader(ref reader, elementType, maxDepth, nextDepth);
+ if (value != null)
+ list.Add(value);
+ }
+
if (targetType.IsArray)
{
- var list = GetOrCreateListFactory(elementType)();
- foreach (var item in element.EnumerateArray())
- list.Add(ReadValue(item, elementType, context, depth + 1));
-
var array = Array.CreateInstance(elementType, list.Count);
list.CopyTo(array, 0);
return array;
}
- IList? targetList = null;
- try
- {
- var instance = Activator.CreateInstance(targetType);
- if (instance is IList list) targetList = list;
- }
- catch { /* Fallback to List */ }
-
- targetList ??= GetOrCreateListFactory(elementType)();
-
- var acObservable = targetList as IAcObservableCollection;
- acObservable?.BeginUpdate();
-
- try
- {
- foreach (var item in element.EnumerateArray())
- targetList.Add(ReadValue(item, elementType, context, depth + 1));
- }
- finally { acObservable?.EndUpdate(); }
-
- return targetList;
+ return list;
}
- private static void MergeIIdCollection(JsonElement arrayElement, object existingCollection, PropertySetterInfo propInfo, DeserializationContext context, int depth)
+ private static object ReadDictionaryFromReader(ref Utf8JsonReader reader, Type keyType, Type valueType, byte maxDepth, int depth)
+ {
+ var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType);
+ var dict = (IDictionary)Activator.CreateInstance(dictType)!;
+ var nextDepth = depth + 1;
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndObject)
+ break;
+
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ continue;
+
+ var keyStr = reader.GetString();
+ if (keyStr == null || !reader.Read())
+ continue;
+
+ object key;
+ if (ReferenceEquals(keyType, StringType)) key = keyStr;
+ else if (ReferenceEquals(keyType, IntType)) key = int.Parse(keyStr, CultureInfo.InvariantCulture);
+ else if (ReferenceEquals(keyType, LongType)) key = long.Parse(keyStr, CultureInfo.InvariantCulture);
+ else if (ReferenceEquals(keyType, GuidType)) key = Guid.Parse(keyStr);
+ else if (keyType.IsEnum) key = Enum.Parse(keyType, keyStr);
+ else key = Convert.ChangeType(keyStr, keyType, CultureInfo.InvariantCulture);
+
+ var value = ReadValueFromReader(ref reader, valueType, maxDepth, nextDepth);
+ dict.Add(key, value);
+ }
+
+ return dict;
+ }
+
+ ///
+ /// Populate object using Utf8JsonReader streaming (no DOM allocation).
+ ///
+ private static void PopulateObjectWithUtf8Reader(string json, object target, DeserializeTypeMetadata metadata, byte maxDepth)
+ {
+ var (buffer, length) = GetUtf8Bytes(json);
+ try
+ {
+ var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
+
+ if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
+ return;
+
+ PopulateObjectMergeFromReader(ref reader, target, metadata, maxDepth, 0);
+ }
+ finally
+ {
+ ReturnUtf8Buffer(buffer);
+ }
+ }
+
+ ///
+ /// Populate list using Utf8JsonReader streaming (no DOM allocation).
+ ///
+ private static void PopulateListWithUtf8Reader(string json, IList targetList, in Type listType, byte maxDepth)
+ {
+ var (buffer, length) = GetUtf8Bytes(json);
+ try
+ {
+ var reader = new Utf8JsonReader(buffer.AsSpan(0, length), new JsonReaderOptions { MaxDepth = maxDepth });
+
+ if (!reader.Read() || reader.TokenType != JsonTokenType.StartArray)
+ return;
+
+ var elementType = GetCollectionElementType(listType);
+ if (elementType == null) return;
+
+ var acObservable = targetList as IAcObservableCollection;
+ acObservable?.BeginUpdate();
+
+ try
+ {
+ targetList.Clear();
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndArray)
+ break;
+
+ var value = ReadValueFromReader(ref reader, elementType, maxDepth, 1);
+ if (value != null)
+ targetList.Add(value);
+ }
+ }
+ finally { acObservable?.EndUpdate(); }
+ }
+ finally
+ {
+ ReturnUtf8Buffer(buffer);
+ }
+ }
+
+ ///
+ /// Populate object with merge semantics from Utf8JsonReader.
+ ///
+ private static void PopulateObjectMergeFromReader(ref Utf8JsonReader reader, object target, DeserializeTypeMetadata metadata, byte maxDepth, int depth)
+ {
+ var propsDict = metadata.PropertySettersFrozen;
+ var nextDepth = depth + 1;
+ var maxDepthReached = nextDepth > maxDepth;
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndObject)
+ break;
+
+ if (reader.TokenType != JsonTokenType.PropertyName)
+ continue;
+
+ var propName = reader.GetString();
+ if (propName == null || !reader.Read())
+ continue;
+
+ if (!propsDict.TryGetValue(propName, out var propInfo))
+ {
+ reader.Skip();
+ continue;
+ }
+
+ var tokenType = reader.TokenType;
+
+ if (maxDepthReached)
+ {
+ if (tokenType != JsonTokenType.StartObject && tokenType != JsonTokenType.StartArray)
+ {
+ // Use cached version for faster primitive reading
+ var primitiveValue = ReadPrimitiveFromReaderCached(ref reader, propInfo);
+ propInfo.SetValue(target, primitiveValue);
+ }
+ else
+ {
+ reader.Skip();
+ }
+ continue;
+ }
+
+ // Handle IId collection merge
+ if (propInfo.IsIIdCollection && tokenType == JsonTokenType.StartArray)
+ {
+ var existingCollection = propInfo.GetValue(target);
+ if (existingCollection != null)
+ {
+ MergeIIdCollectionFromReader(ref reader, existingCollection, propInfo, maxDepth, depth);
+ continue;
+ }
+ }
+
+ // Handle nested objects - merge into existing
+ if (tokenType == JsonTokenType.StartObject && !propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
+ {
+ var existingObj = propInfo.GetValue(target);
+ if (existingObj != null)
+ {
+ var nestedMetadata = GetTypeMetadata(propInfo.PropertyType);
+ PopulateObjectMergeFromReader(ref reader, existingObj, nestedMetadata, maxDepth, nextDepth);
+ continue;
+ }
+ }
+
+ // Use cached version for faster type resolution
+ var value = ReadValueFromReaderCached(ref reader, propInfo, maxDepth, nextDepth);
+ propInfo.SetValue(target, value);
+ }
+ }
+
+ ///
+ /// Read primitive value from Utf8JsonReader using cached PropertySetterInfo.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static object? ReadPrimitiveFromReaderCached(ref Utf8JsonReader reader, PropertySetterInfo propInfo)
+ {
+ var tokenType = reader.TokenType;
+ if (tokenType == JsonTokenType.Null) return null;
+ if (tokenType == JsonTokenType.True) return true;
+ if (tokenType == JsonTokenType.False) return false;
+ if (tokenType == JsonTokenType.Number) return ReadNumberFromReaderCached(ref reader, propInfo);
+ if (tokenType == JsonTokenType.String) return ReadStringFromReaderCached(ref reader, propInfo);
+ return null;
+ }
+
+ ///
+ /// Merge IId collection from Utf8JsonReader streaming.
+ ///
+ private static void MergeIIdCollectionFromReader(ref Utf8JsonReader reader, object existingCollection, PropertySetterInfo propInfo, byte maxDepth, int depth)
{
var elementType = propInfo.ElementType!;
var idGetter = propInfo.ElementIdGetter!;
@@ -481,6 +822,364 @@ public static class AcJsonDeserializer
}
}
+ var nextDepth = depth + 1;
+ var elementMetadata = GetTypeMetadata(elementType);
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndArray)
+ break;
+
+ if (reader.TokenType != JsonTokenType.StartObject)
+ {
+ var primitiveValue = ReadValueFromReader(ref reader, elementType, maxDepth, nextDepth);
+ if (primitiveValue != null) existingList.Add(primitiveValue);
+ continue;
+ }
+
+ // For objects, we need to read the full object to check the Id
+ var newItem = ReadObjectFromReaderAtStart(ref reader, elementType, elementMetadata, maxDepth, nextDepth);
+ if (newItem == null) continue;
+
+ // Check if this item already exists by Id
+ var itemId = propInfo.ElementIdGetter?.Invoke(newItem);
+ if (itemId != null && !IsDefaultValue(itemId, idType) && existingById != null)
+ {
+ if (existingById.TryGetValue(itemId, out var existingItem))
+ {
+ // Copy properties from newItem to existingItem
+ CopyProperties(newItem, existingItem, elementMetadata);
+ continue;
+ }
+ }
+
+ existingList.Add(newItem);
+ }
+ }
+ finally { acObservable?.EndUpdate(); }
+ }
+
+ ///
+ /// Copy properties from source to target using metadata.
+ ///
+ private static void CopyProperties(object source, object target, DeserializeTypeMetadata metadata)
+ {
+ foreach (var prop in metadata.PropertySettersFrozen.Values)
+ {
+ var value = prop.GetValue(source);
+ if (value != null)
+ prop.SetValue(target, value);
+ }
+ }
+
+ ///
+ /// Read primitive value from Utf8JsonReader.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static object? ReadPrimitiveFromReader(ref Utf8JsonReader reader, Type targetType)
+ {
+ var tokenType = reader.TokenType;
+ if (tokenType == JsonTokenType.Null) return null;
+ if (tokenType == JsonTokenType.True) return true;
+ if (tokenType == JsonTokenType.False) return false;
+ if (tokenType == JsonTokenType.Number) return ReadNumberFromReader(ref reader, targetType);
+ if (tokenType == JsonTokenType.String) return ReadStringFromReader(ref reader, targetType);
+ return null;
+ }
+
+ #endregion
+
+ #region Validation
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void ValidateJson(string json, in Type targetType)
+ {
+ if (json.Length < 2) return;
+
+ var firstChar = json[0];
+
+ // Quick check for double-serialized JSON
+ if (firstChar == '"' && json[^1] == '"')
+ {
+ var inner = json.AsSpan(1, json.Length - 2);
+ if (inner.Contains("\\\"", StringComparison.Ordinal) &&
+ (inner.Contains("{", StringComparison.Ordinal) || inner.Contains("[", StringComparison.Ordinal)))
+ throw new AcJsonDeserializationException(
+ $"Detected double-serialized JSON string. Target type: {targetType.Name}.",
+ json, targetType);
+ }
+
+ var isArrayJson = firstChar == '[';
+ var isObjectJson = firstChar == '{';
+
+ if (isArrayJson)
+ {
+ var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType);
+ var isDictType = IsDictionaryType(targetType, out _, out _);
+ if (!isCollectionType && !isDictType && targetType != typeof(object))
+ throw new AcJsonDeserializationException($"JSON is an array but target type '{targetType.Name}' is not a collection type.", json, targetType);
+ }
+ else if (isObjectJson)
+ {
+ var isCollectionType = targetType.IsArray || IsGenericCollectionType(targetType);
+ var isDictType = IsDictionaryType(targetType, out _, out _);
+ if (isCollectionType && !isDictType)
+ throw new AcJsonDeserializationException($"JSON is an object but target type '{targetType.Name}' is a collection type (not dictionary).", json, targetType);
+ }
+ }
+
+ #endregion
+
+ #region With Reference Handling (JsonElement Path)
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static object? ReadValue(in JsonElement element, in Type targetType, DeserializationContext context, int depth)
+ {
+ var kind = element.ValueKind;
+ if (kind == JsonValueKind.Object) return ReadObject(element, targetType, context, depth);
+ if (kind == JsonValueKind.Array) return ReadArray(element, targetType, context, depth);
+ if (kind == JsonValueKind.Null || kind == JsonValueKind.Undefined) return null;
+ return ReadPrimitive(element, targetType, kind);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static object? ReadObject(in JsonElement element, in Type targetType, DeserializationContext context, int depth)
+ {
+ // Check for $ref first
+ if (element.TryGetProperty(RefPropertyUtf8, out var refElement))
+ {
+ var refId = refElement.GetString()!;
+ return context.TryGetReferencedObject(refId, out var refObj) ? refObj : new DeferredReference(refId, targetType);
+ }
+
+ if (depth > context.MaxDepth) return null;
+
+ if (IsDictionaryType(targetType, out var keyType, out var valueType))
+ return ReadDictionary(element, keyType!, valueType!, context, depth);
+
+ var metadata = GetTypeMetadata(targetType);
+
+ var instance = metadata.CompiledConstructor?.Invoke();
+ if (instance == null)
+ {
+ try { instance = Activator.CreateInstance(targetType); }
+ catch (MissingMethodException ex)
+ {
+ throw new AcJsonDeserializationException(
+ $"Cannot deserialize type '{targetType.FullName}' because it does not have a parameterless constructor.",
+ null, targetType, ex);
+ }
+ }
+
+ if (instance == null) return null;
+
+ // Check for $id and register
+ if (element.TryGetProperty(IdPropertyUtf8, out var idElement))
+ context.RegisterObject(idElement.GetString()!, instance);
+
+ PopulateObjectInternal(element, instance, metadata, context, depth);
+
+ return instance;
+ }
+
+ private static void PopulateObjectInternal(in JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth)
+ {
+ var propsDict = metadata.PropertySettersFrozen;
+ var nextDepth = depth + 1;
+
+ foreach (var jsonProp in element.EnumerateObject())
+ {
+ var propName = jsonProp.Name;
+
+ // Skip $ properties
+ if (propName.Length > 0 && propName[0] == '$') continue;
+
+ if (!propsDict.TryGetValue(propName, out var propInfo)) continue;
+
+ var value = ReadValue(jsonProp.Value, propInfo.PropertyType, context, nextDepth);
+
+ if (value is DeferredReference deferred)
+ context.AddPropertyToResolve(target, propInfo, deferred.RefId);
+ else
+ propInfo.SetValue(target, value);
+ }
+ }
+
+ private static void PopulateObjectInternalMerge(in JsonElement element, object target, DeserializeTypeMetadata metadata, DeserializationContext context, int depth)
+ {
+ var propsDict = metadata.PropertySettersFrozen;
+ var nextDepth = depth + 1;
+ var maxDepthReached = nextDepth > context.MaxDepth;
+
+ foreach (var jsonProp in element.EnumerateObject())
+ {
+ var propName = jsonProp.Name;
+
+ // Skip $ properties
+ if (propName.Length > 0 && propName[0] == '$') continue;
+
+ if (!propsDict.TryGetValue(propName, out var propInfo)) continue;
+
+ var propValue = jsonProp.Value;
+ var propValueKind = propValue.ValueKind;
+
+ if (maxDepthReached)
+ {
+ if (propValueKind != JsonValueKind.Object && propValueKind != JsonValueKind.Array)
+ {
+ var primitiveValue = ReadPrimitive(propValue, propInfo.PropertyType, propValueKind);
+ propInfo.SetValue(target, primitiveValue);
+ }
+ continue;
+ }
+
+ // Handle IId collection merge
+ if (propInfo.IsIIdCollection && propValueKind == JsonValueKind.Array)
+ {
+ var existingCollection = propInfo.GetValue(target);
+ if (existingCollection != null)
+ {
+ MergeIIdCollection(propValue, existingCollection, propInfo, context, depth);
+ continue;
+ }
+ }
+
+ // Handle nested objects
+ if (propValueKind == JsonValueKind.Object)
+ {
+ // Check for $ref
+ if (propValue.TryGetProperty(RefPropertyUtf8, out _))
+ {
+ var value = ReadValue(propValue, propInfo.PropertyType, context, nextDepth);
+ if (value is DeferredReference deferred)
+ context.AddPropertyToResolve(target, propInfo, deferred.RefId);
+ else
+ propInfo.SetValue(target, value);
+ continue;
+ }
+
+ // Merge into existing object
+ if (!propInfo.PropertyType.IsPrimitive && !ReferenceEquals(propInfo.PropertyType, StringType))
+ {
+ var existingObj = propInfo.GetValue(target);
+ if (existingObj != null)
+ {
+ var nestedMetadata = GetTypeMetadata(propInfo.PropertyType);
+ PopulateObjectInternalMerge(propValue, existingObj, nestedMetadata, context, nextDepth);
+ continue;
+ }
+ }
+ }
+
+ var value2 = ReadValue(propValue, propInfo.PropertyType, context, nextDepth);
+
+ if (value2 is DeferredReference deferred2)
+ context.AddPropertyToResolve(target, propInfo, deferred2.RefId);
+ else
+ propInfo.SetValue(target, value2);
+ }
+ }
+
+ private static void PopulateList(in JsonElement arrayElement, IList targetList, in Type listType, DeserializationContext context, int depth)
+ {
+ if (depth > context.MaxDepth) return;
+
+ var elementType = GetCollectionElementType(listType);
+ if (elementType == null) return;
+
+ var acObservable = targetList as IAcObservableCollection;
+ acObservable?.BeginUpdate();
+
+ try
+ {
+ targetList.Clear();
+ var nextDepth = depth + 1;
+
+ foreach (var item in arrayElement.EnumerateArray())
+ {
+ var value = ReadValue(item, elementType, context, nextDepth);
+ if (value != null)
+ targetList.Add(value);
+ }
+ }
+ finally { acObservable?.EndUpdate(); }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static object? ReadArray(in JsonElement element, in Type targetType, DeserializationContext context, int depth)
+ {
+ if (depth > context.MaxDepth) return null;
+
+ var elementType = GetCollectionElementType(targetType);
+ if (elementType == null) return null;
+
+ var nextDepth = depth + 1;
+
+ if (targetType.IsArray)
+ {
+ var list = GetOrCreateListFactory(elementType)();
+ foreach (var item in element.EnumerateArray())
+ list.Add(ReadValue(item, elementType, context, nextDepth));
+
+ var array = Array.CreateInstance(elementType, list.Count);
+ list.CopyTo(array, 0);
+ return array;
+ }
+
+ IList? targetList = null;
+ try
+ {
+ var instance = Activator.CreateInstance(targetType);
+ if (instance is IList list) targetList = list;
+ }
+ catch { /* Fallback to List */ }
+
+ targetList ??= GetOrCreateListFactory(elementType)();
+
+ var acObservable = targetList as IAcObservableCollection;
+ acObservable?.BeginUpdate();
+
+ try
+ {
+ foreach (var item in element.EnumerateArray())
+ targetList.Add(ReadValue(item, elementType, context, nextDepth));
+ }
+ finally { acObservable?.EndUpdate(); }
+
+ return targetList;
+ }
+
+ private static void MergeIIdCollection(in JsonElement arrayElement, object existingCollection, PropertySetterInfo propInfo, DeserializationContext context, int depth)
+ {
+ var elementType = propInfo.ElementType!;
+ var idGetter = propInfo.ElementIdGetter!;
+ var idType = propInfo.ElementIdType!;
+
+ var existingList = (IList)existingCollection;
+ var count = existingList.Count;
+
+ var acObservable = existingList as IAcObservableCollection;
+ acObservable?.BeginUpdate();
+
+ try
+ {
+ Dictionary