AyCode.Core/AyCode.Core/Serializers/Jsons/AcJsonDeserializer.cs

751 lines
28 KiB
C#

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;
/// <summary>
/// Exception thrown when JSON deserialization fails.
/// </summary>
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;
}
}
/// <summary>
/// High-performance custom JSON deserializer optimized for IId&lt;T&gt; reference handling.
/// Uses Utf8JsonReader for streaming deserialization when possible (STJ approach).
/// </summary>
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
/// <summary>
/// Deserialize JSON string to a new object of type T with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T? Deserialize<T>(string json) => Deserialize<T>(json, AcJsonSerializerOptions.Default);
/// <summary>
/// Deserialize UTF-8 encoded JSON bytes to a new object of type T with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T? Deserialize<T>(ReadOnlySpan<byte> utf8Json) => Deserialize<T>(utf8Json, AcJsonSerializerOptions.Default);
/// <summary>
/// Deserialize UTF-8 encoded JSON bytes to a new object of type T with specified options.
/// </summary>
public static T? Deserialize<T>(ReadOnlySpan<byte> 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);
}
}
/// <summary>
/// Deserialize JSON string to a new object of type T with specified options.
/// </summary>
public static T? Deserialize<T>(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<T>(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);
}
}
/// <summary>
/// Deserialize JSON string to specified type with specified options.
/// </summary>
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);
}
}
/// <summary>
/// Deserialize Expression from JSON.
/// Uses AcSerializerCommon for entity type extraction.
/// </summary>
private static Expression? DeserializeExpression(ReadOnlySpan<byte> 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);
}
/// <summary>
/// Checks if a type is an Expression type.
/// Uses AcSerializerCommon for consistency.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsExpressionType(Type type)
{
return AcSerializerCommon.IsExpressionType(type);
}
/// <summary>
/// Deserialize UTF-8 encoded JSON bytes to specified type with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static object? Deserialize(ReadOnlySpan<byte> utf8Json, Type targetType)
=> Deserialize(utf8Json, targetType, AcJsonSerializerOptions.Default);
/// <summary>
/// Deserialize UTF-8 encoded JSON bytes to specified type with specified options.
/// </summary>
public static object? Deserialize(ReadOnlySpan<byte> 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);
}
}
/// <summary>
/// Deserialize JSON string to specified type with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static object? Deserialize(string json, Type targetType) => Deserialize(json, targetType, AcJsonSerializerOptions.Default);
/// <summary>
/// Populate existing object with JSON data (merge mode) with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate<T>(string json, T target) where T : class
=> Populate(json, target, AcJsonSerializerOptions.Default);
/// <summary>
/// Populate existing object with JSON data (merge mode) with specified options.
/// </summary>
public static void Populate<T>(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);
}
/// <summary>
/// Populate existing object with JSON data with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate(string json, object target)
=> Populate(json, target, target.GetType(), AcJsonSerializerOptions.Default);
/// <summary>
/// Populate existing object with JSON data with specified options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate(string json, object target, in AcJsonSerializerOptions options)
=> Populate(json, target, target.GetType(), options);
/// <summary>
/// Populate existing object with JSON data with default options.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Populate(string json, object target, Type targetType)
=> Populate(json, target, targetType, AcJsonSerializerOptions.Default);
/// <summary>
/// Populate existing object with JSON data with specified options.
/// </summary>
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
/// <summary>
/// Create a deserialize chain that parses JSON once and allows multiple deserializations.
/// Efficient for deserializing the same JSON to multiple different types.
/// </summary>
public static IDeserializeChain<T> CreateDeserializeChain<T>(string json)
=> CreateDeserializeChain<T>(json, AcJsonSerializerOptions.Default);
/// <summary>
/// Create a deserialize chain with options.
/// </summary>
public static IDeserializeChain<T> CreateDeserializeChain<T>(string json, in AcJsonSerializerOptions options)
{
if (string.IsNullOrEmpty(json) || json == "null")
return DeserializeChain<T>.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<T>(doc, context, options, (T?)result);
}
catch
{
DeserializationContextPool.Return(context);
doc.Dispose();
throw;
}
}
/// <summary>
/// Create a populate chain that parses JSON once and allows populating multiple objects.
/// Efficient for populating multiple objects from the same JSON source.
/// </summary>
public static IPopulateChain CreatePopulateChain(string json, object target)
=> CreatePopulateChain(json, target, AcJsonSerializerOptions.Default);
/// <summary>
/// Create a populate chain with options.
/// </summary>
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;
}
}
/// <summary>
/// Internal helper: populate object from parsed JsonDocument.
/// Reuses existing populate logic without reparsing.
/// </summary>
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)
/// <summary>
/// Implementation of deserialize chain.
/// </summary>
private sealed class DeserializeChain<T> : IDeserializeChain<T>
{
public static readonly IDeserializeChain<T> 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<TOther>()
{
if (_document == null || _context == null)
throw new ObjectDisposedException(nameof(DeserializeChain<T>));
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<T>
{
public T? Value => default;
public TOther? ThenDeserialize<TOther>() => default;
public void Dispose() { }
}
}
/// <summary>
/// Implementation of populate chain.
/// </summary>
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
/// <summary>
/// Represents a deserialize chain that allows multiple deserializations from the same parsed JSON.
/// Implements IDisposable - call Dispose() when done or use 'using' statement.
/// </summary>
public interface IDeserializeChain<T> : IDisposable
{
/// <summary>
/// The first deserialized value.
/// </summary>
T? Value { get; }
/// <summary>
/// Deserialize to another type from the same JSON.
/// </summary>
TOther? ThenDeserialize<TOther>();
}
/// <summary>
/// Represents a populate chain that allows populating multiple objects from the same parsed JSON.
/// Implements IDisposable - call Dispose() when done or use 'using' statement.
/// </summary>
public interface IPopulateChain : IDisposable
{
/// <summary>
/// Populate another object from the same JSON.
/// </summary>
IPopulateChain ThenPopulate(object target);
}
#endregion