using System.Collections; using System.Linq.Expressions; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using AyCode.Core.Helpers; using AyCode.Core.Serializers.Expressions; using static AyCode.Core.Helpers.JsonUtilities; namespace AyCode.Core.Serializers.Jsons; /// /// Exception thrown when JSON deserialization fails. /// public class AcJsonDeserializationException : Exception { public string? Json { get; } public Type? TargetType { get; } public AcJsonDeserializationException(string message, string? json = null, Type? targetType = null, Exception? innerException = null) : base(message, innerException) { Json = json?.Length > 500 ? json[..500] + "..." : json; TargetType = targetType; } } /// /// High-performance custom JSON deserializer optimized for IId<T> reference handling. /// Uses Utf8JsonReader for streaming deserialization when possible (STJ approach). /// public static partial class AcJsonDeserializer { // 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(); private static readonly Type ExpressionBaseType = typeof(Expression); #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 UTF-8 encoded JSON bytes to a new object of type T with default options. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static T? Deserialize(ReadOnlySpan utf8Json) => Deserialize(utf8Json, AcJsonSerializerOptions.Default); /// /// Deserialize UTF-8 encoded JSON bytes to a new object of type T with specified options. /// public static T? Deserialize(ReadOnlySpan utf8Json, in AcJsonSerializerOptions options) { if (utf8Json.IsEmpty) return default; if (utf8Json.Length == 4 && utf8Json.SequenceEqual("null"u8)) return default; var targetType = typeof(T); // Handle Expression types if (IsExpressionType(targetType)) { return (T?)(object?)DeserializeExpression(utf8Json, targetType, options); } try { if (!options.UseReferenceHandling) { var reader = new Utf8JsonReader(utf8Json, new JsonReaderOptions { MaxDepth = options.MaxDepth }); if (!reader.Read()) return default; return (T?)ReadValueFromReader(ref reader, targetType, options.MaxDepth, 0); } var jsonBytes = utf8Json.ToArray(); using var doc = JsonDocument.Parse(jsonBytes); 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) { throw new AcJsonDeserializationException($"Failed to parse JSON for type '{targetType.Name}': {ex.Message}", null, targetType, ex); } catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { throw new AcJsonDeserializationException($"Failed to convert JSON value for type '{targetType.Name}': {ex.Message}", null, targetType, ex); } } /// /// Deserialize JSON string to a new object of type T with specified options. /// public static T? Deserialize(string json, in AcJsonSerializerOptions options) { if (string.IsNullOrEmpty(json) || json == "null") return default; var targetType = typeof(T); // Handle Expression types if (IsExpressionType(targetType)) { var (buffer, length) = GetUtf8Bytes(json); try { return (T?)(object?)DeserializeExpression(buffer.AsSpan(0, length), targetType, options); } finally { ReturnUtf8Buffer(buffer); } } try { if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult)) return (T?)primitiveResult; ValidateJson(json, targetType); if (!options.UseReferenceHandling) { return DeserializeWithUtf8Reader(json, options.MaxDepth); } using var doc = JsonDocument.Parse(json); 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) { throw new AcJsonDeserializationException($"Failed to parse JSON for type '{targetType.Name}': {ex.Message}", json, targetType, ex); } catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { throw new AcJsonDeserializationException($"Failed to convert JSON value for type '{targetType.Name}': {ex.Message}", json, targetType, ex); } } /// /// Deserialize JSON string to specified type with specified options. /// public static object? Deserialize(string json, in Type targetType, in AcJsonSerializerOptions options) { if (string.IsNullOrEmpty(json) || json == "null") return null; // Handle Expression types - deserialize as AcExpressionNode and rebuild if (AcSerializerCommon.IsExpressionType(targetType)) { var (buffer, length) = GetUtf8Bytes(json); try { return DeserializeExpression(buffer.AsSpan(0, length), targetType, options); } finally { ReturnUtf8Buffer(buffer); } } try { var firstChar = json[0]; var isArrayJson = firstChar == '['; var isObjectJson = firstChar == '{'; if (!isArrayJson && !isObjectJson) { if (TryDeserializePrimitiveFast(json, targetType, out var primitiveResult)) return primitiveResult; } ValidateJson(json, targetType); // Fast path for no reference handling if (!options.UseReferenceHandling) { return DeserializeWithUtf8ReaderNonGeneric(json, targetType, options.MaxDepth); } // Reference handling requires DOM - copy to array for JsonDocument.Parse var jsonBytes = Encoding.UTF8.GetBytes(json); using var doc = JsonDocument.Parse(jsonBytes); 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) { throw new AcJsonDeserializationException($"Failed to parse JSON for type '{targetType.Name}': {ex.Message}", json, targetType, ex); } catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { throw new AcJsonDeserializationException($"Failed to convert JSON value for type '{targetType.Name}': {ex.Message}", json, targetType, ex); } } /// /// Deserialize Expression from JSON. /// Uses AcSerializerCommon for entity type extraction. /// private static Expression? DeserializeExpression(ReadOnlySpan utf8Json, Type targetExpressionType, in AcJsonSerializerOptions options) { var reader = new Utf8JsonReader(utf8Json, new JsonReaderOptions { MaxDepth = options.MaxDepth }); if (!reader.Read()) return null; var node = (AcExpressionNode?)ReadValueFromReader(ref reader, typeof(AcExpressionNode), options.MaxDepth, 0); if (node == null) return null; var entityType = AcSerializerCommon.GetExpressionEntityType(targetExpressionType); return AcExpressionRebuilder.FromNode(node, entityType); } /// /// Checks if a type is an Expression type. /// Uses AcSerializerCommon for consistency. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsExpressionType(Type type) { return AcSerializerCommon.IsExpressionType(type); } /// /// Deserialize UTF-8 encoded JSON bytes to specified type with default options. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static object? Deserialize(ReadOnlySpan utf8Json, Type targetType) => Deserialize(utf8Json, targetType, AcJsonSerializerOptions.Default); /// /// Deserialize UTF-8 encoded JSON bytes to specified type with specified options. /// public static object? Deserialize(ReadOnlySpan utf8Json, in Type targetType, in AcJsonSerializerOptions options) { if (utf8Json.IsEmpty) return null; // Check for "null" literal if (utf8Json.Length == 4 && utf8Json.SequenceEqual("null"u8)) return null; // Handle Expression types if (AcSerializerCommon.IsExpressionType(targetType)) { return DeserializeExpression(utf8Json, targetType, options); } try { // Fast path for no reference handling if (!options.UseReferenceHandling) { var reader = new Utf8JsonReader(utf8Json, new JsonReaderOptions { MaxDepth = options.MaxDepth }); if (!reader.Read()) return null; return ReadValueFromReader(ref reader, targetType, options.MaxDepth, 0); } // Reference handling requires DOM - copy to array for JsonDocument.Parse var jsonBytes = utf8Json.ToArray(); using var doc = JsonDocument.Parse(jsonBytes); 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) { throw new AcJsonDeserializationException($"Failed to parse JSON for type '{targetType.Name}': {ex.Message}", null, targetType, ex); } catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { throw new AcJsonDeserializationException($"Failed to convert JSON value for type '{targetType.Name}': {ex.Message}", null, targetType, ex); } } /// /// 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); /// /// 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, in AcJsonSerializerOptions options) where T : class { ArgumentNullException.ThrowIfNull(target); if (string.IsNullOrEmpty(json) || json == "null") return; 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. /// [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, 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); // 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; var context = DeserializationContextPool.Get(options); context.IsMergeMode = true; try { 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 object with JSON value of kind '{rootElement.ValueKind}'", json, targetType); context.ResolveReferences(); } finally { DeserializationContextPool.Return(context); } } catch (AcJsonDeserializationException) { throw; } catch (System.Text.Json.JsonException ex) { throw new AcJsonDeserializationException($"Failed to parse JSON for population of type '{targetType.Name}': {ex.Message}", json, targetType, ex); } catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { throw new AcJsonDeserializationException($"Failed to convert JSON value during population of type '{targetType.Name}': {ex.Message}", json, targetType, ex); } } #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 Chain API /// /// Create a deserialize chain that parses JSON once and allows multiple deserializations. /// Efficient for deserializing the same JSON to multiple different types. /// public static IDeserializeChain CreateDeserializeChain(string json) => CreateDeserializeChain(json, AcJsonSerializerOptions.Default); /// /// Create a deserialize chain with options. /// public static IDeserializeChain CreateDeserializeChain(string json, in AcJsonSerializerOptions options) { if (string.IsNullOrEmpty(json) || json == "null") return DeserializeChain.Empty; var targetType = typeof(T); ValidateJson(json, targetType); var doc = JsonDocument.Parse(json); var context = DeserializationContextPool.Get(options); try { var result = ReadValue(doc.RootElement, targetType, context, 0); context.ResolveReferences(); return new DeserializeChain(doc, context, options, (T?)result); } catch { DeserializationContextPool.Return(context); doc.Dispose(); throw; } } /// /// Create a populate chain that parses JSON once and allows populating multiple objects. /// Efficient for populating multiple objects from the same JSON source. /// public static IPopulateChain CreatePopulateChain(string json, object target) => CreatePopulateChain(json, target, AcJsonSerializerOptions.Default); /// /// Create a populate chain with options. /// public static IPopulateChain CreatePopulateChain(string json, object target, in AcJsonSerializerOptions options) { ArgumentNullException.ThrowIfNull(target); if (string.IsNullOrEmpty(json) || json == "null") return PopulateChain.Empty; var targetType = target.GetType(); ValidateJson(json, targetType); var doc = JsonDocument.Parse(json); var context = DeserializationContextPool.Get(options); context.IsMergeMode = true; try { PopulateFromDocument(doc.RootElement, target, targetType, context); context.ResolveReferences(); return new PopulateChain(doc, context, options); } catch { DeserializationContextPool.Return(context); doc.Dispose(); throw; } } /// /// Internal helper: populate object from parsed JsonDocument. /// Reuses existing populate logic without reparsing. /// private static void PopulateFromDocument(in JsonElement rootElement, object target, in Type targetType, DeserializationContext context) { 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", null, targetType); } else if (rootElement.ValueKind == JsonValueKind.Object) { var metadata = GetTypeMetadata(targetType); PopulateObjectInternalMerge(rootElement, target, metadata, context, 0); } else throw new AcJsonDeserializationException($"Cannot populate object with JSON value of kind '{rootElement.ValueKind}'", null, targetType); } #endregion #region Chain Implementations (Nested Classes) /// /// Implementation of deserialize chain. /// private sealed class DeserializeChain : IDeserializeChain { public static readonly IDeserializeChain Empty = new EmptyDeserializeChain(); private JsonDocument? _document; private DeserializationContext? _context; private readonly AcJsonSerializerOptions _options; public T? Value { get; } public DeserializeChain(JsonDocument document, DeserializationContext context, AcJsonSerializerOptions options, T? value) { _document = document; _context = context; _options = options; Value = value; } public TOther? ThenDeserialize() { if (_document == null || _context == null) throw new ObjectDisposedException(nameof(DeserializeChain)); var targetType = typeof(TOther); try { var result = ReadValue(_document.RootElement, targetType, _context, 0); _context.ResolveReferences(); return (TOther?)result; } catch (AcJsonDeserializationException) { throw; } catch (Exception ex) { throw new AcJsonDeserializationException( $"Failed to deserialize to type '{targetType.Name}' in chain: {ex.Message}", null, targetType, ex); } } public void Dispose() { if (_context != null) { DeserializationContextPool.Return(_context); _context = null; } if (_document != null) { _document.Dispose(); _document = null; } } private sealed class EmptyDeserializeChain : IDeserializeChain { public T? Value => default; public TOther? ThenDeserialize() => default; public void Dispose() { } } } /// /// Implementation of populate chain. /// private sealed class PopulateChain : IPopulateChain { public static readonly IPopulateChain Empty = new EmptyPopulateChain(); private JsonDocument? _document; private DeserializationContext? _context; private readonly AcJsonSerializerOptions _options; public PopulateChain(JsonDocument document, DeserializationContext context, AcJsonSerializerOptions options) { _document = document; _context = context; _options = options; } public IPopulateChain ThenPopulate(object target) { ArgumentNullException.ThrowIfNull(target); if (_document == null || _context == null) throw new ObjectDisposedException(nameof(PopulateChain)); var targetType = target.GetType(); try { PopulateFromDocument(_document.RootElement, target, targetType, _context); _context.ResolveReferences(); return this; } catch (AcJsonDeserializationException) { throw; } catch (Exception ex) { throw new AcJsonDeserializationException( $"Failed to populate object of type '{targetType.Name}' in chain: {ex.Message}", null, targetType, ex); } } public void Dispose() { if (_context != null) { DeserializationContextPool.Return(_context); _context = null; } if (_document != null) { _document.Dispose(); _document = null; } } private sealed class EmptyPopulateChain : IPopulateChain { public IPopulateChain ThenPopulate(object target) => this; public void Dispose() { } } } #endregion } #region Chain Public Interfaces /// /// Represents a deserialize chain that allows multiple deserializations from the same parsed JSON. /// Implements IDisposable - call Dispose() when done or use 'using' statement. /// public interface IDeserializeChain : IDisposable { /// /// The first deserialized value. /// T? Value { get; } /// /// Deserialize to another type from the same JSON. /// TOther? ThenDeserialize(); } /// /// Represents a populate chain that allows populating multiple objects from the same parsed JSON. /// Implements IDisposable - call Dispose() when done or use 'using' statement. /// public interface IPopulateChain : IDisposable { /// /// Populate another object from the same JSON. /// IPopulateChain ThenPopulate(object target); } #endregion