// MIT License - Copyright (c) 2023 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Serialization { using System; using System.Buffers; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Reflection; using System.Runtime.Serialization.Formatters.Binary; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using JsonConverters; using ProtoBuf; using WallstopStudios.UnityHelpers.Core.DataStructure.Adapters; using WallstopStudios.UnityHelpers.Core.Helper; using WallstopStudios.UnityHelpers.Utils; using TypeConverter = JsonConverters.TypeConverter; internal static class SerializerEncoding { public static readonly Encoding Encoding; public static readonly JsonSerializerOptions NormalJsonOptions; public static readonly JsonSerializerOptions PrettyJsonOptions; public static readonly JsonSerializerOptions FastJsonOptions; public static readonly JsonSerializerOptions FastPocoJsonOptions; public static JsonSerializerOptions GetNormalJsonOptions() { return new JsonSerializerOptions { IgnoreReadOnlyFields = false, IgnoreReadOnlyProperties = false, ReferenceHandler = ReferenceHandler.IgnoreCycles, IncludeFields = true, PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals | JsonNumberHandling.AllowReadingFromString, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, Converters = { WGuidConverter.Instance, RangeConverterFactory.Instance, FastVector2IntConverter.Instance, FastVector3IntConverter.Instance, new JsonStringEnumConverter(), Vector3Converter.Instance, Vector2Converter.Instance, Vector4Converter.Instance, Vector2IntConverter.Instance, Vector3IntConverter.Instance, Matrix4x4Converter.Instance, QuaternionConverter.Instance, LayerMaskConverter.Instance, ResolutionConverter.Instance, RenderTextureDescriptorConverter.Instance, MinMaxCurveConverter.Instance, MinMaxGradientConverter.Instance, ColorBlockConverter.Instance, BoundingSphereConverter.Instance, RaycastHitConverter.Instance, TouchConverter.Instance, SceneConverter.Instance, PoseConverter.Instance, PlaneConverter.Instance, RayConverter.Instance, Ray2DConverter.Instance, RectOffsetConverter.Instance, RangeIntConverter.Instance, Hash128Converter.Instance, AnimationCurveConverter.Instance, GradientConverter.Instance, SphericalHarmonicsL2Converter.Instance, TypeConverter.Instance, GameObjectConverter.Instance, ColorConverter.Instance, Color32Converter.Instance, RectConverter.Instance, RectIntConverter.Instance, BoundsConverter.Instance, BoundsIntConverter.Instance, BitSetConverter.Instance, ImmutableBitSetConverter.Instance, DequeConverterFactory.Instance, CyclicBufferConverterFactory.Instance, SerializableSetConverterFactory.Instance, SerializableDictionaryConverterFactory.Instance, SerializableSortedDictionaryConverterFactory.Instance, }, }; } public static JsonSerializerOptions GetPrettyJsonOptions() { return new JsonSerializerOptions { IgnoreReadOnlyFields = false, IgnoreReadOnlyProperties = false, ReferenceHandler = ReferenceHandler.IgnoreCycles, PropertyNameCaseInsensitive = true, IncludeFields = true, NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals | JsonNumberHandling.AllowReadingFromString, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, Converters = { WGuidConverter.Instance, RangeConverterFactory.Instance, FastVector2IntConverter.Instance, FastVector3IntConverter.Instance, new JsonStringEnumConverter(), Vector3Converter.Instance, Vector2Converter.Instance, Vector4Converter.Instance, Vector2IntConverter.Instance, Vector3IntConverter.Instance, Matrix4x4Converter.Instance, QuaternionConverter.Instance, LayerMaskConverter.Instance, ResolutionConverter.Instance, RenderTextureDescriptorConverter.Instance, MinMaxCurveConverter.Instance, MinMaxGradientConverter.Instance, ColorBlockConverter.Instance, BoundingSphereConverter.Instance, RaycastHitConverter.Instance, TouchConverter.Instance, SceneConverter.Instance, PoseConverter.Instance, PlaneConverter.Instance, RayConverter.Instance, Ray2DConverter.Instance, RectOffsetConverter.Instance, RangeIntConverter.Instance, Hash128Converter.Instance, AnimationCurveConverter.Instance, GradientConverter.Instance, SphericalHarmonicsL2Converter.Instance, TypeConverter.Instance, GameObjectConverter.Instance, ColorConverter.Instance, Color32Converter.Instance, RectConverter.Instance, RectIntConverter.Instance, BoundsConverter.Instance, BoundsIntConverter.Instance, BitSetConverter.Instance, ImmutableBitSetConverter.Instance, DequeConverterFactory.Instance, CyclicBufferConverterFactory.Instance, SerializableSetConverterFactory.Instance, SerializableDictionaryConverterFactory.Instance, SerializableSortedDictionaryConverterFactory.Instance, }, WriteIndented = true, }; } public static JsonSerializerOptions GetFastJsonOptions() { return new JsonSerializerOptions { IgnoreReadOnlyFields = false, IgnoreReadOnlyProperties = true, ReferenceHandler = null, PropertyNameCaseInsensitive = false, IncludeFields = false, NumberHandling = JsonNumberHandling.Strict, ReadCommentHandling = JsonCommentHandling.Disallow, AllowTrailingCommas = false, Converters = { WGuidConverter.Instance, RangeConverterFactory.Instance, FastVector2IntConverter.Instance, FastVector3IntConverter.Instance, Vector3Converter.Instance, Vector2Converter.Instance, Vector4Converter.Instance, Vector2IntConverter.Instance, Vector3IntConverter.Instance, Matrix4x4Converter.Instance, QuaternionConverter.Instance, LayerMaskConverter.Instance, ResolutionConverter.Instance, RenderTextureDescriptorConverter.Instance, MinMaxCurveConverter.Instance, MinMaxGradientConverter.Instance, ColorBlockConverter.Instance, BoundingSphereConverter.Instance, RaycastHitConverter.Instance, TouchConverter.Instance, SceneConverter.Instance, PoseConverter.Instance, PlaneConverter.Instance, RayConverter.Instance, Ray2DConverter.Instance, RectOffsetConverter.Instance, RangeIntConverter.Instance, Hash128Converter.Instance, AnimationCurveConverter.Instance, GradientConverter.Instance, SphericalHarmonicsL2Converter.Instance, TypeConverter.Instance, GameObjectConverter.Instance, ColorConverter.Instance, Color32Converter.Instance, RectConverter.Instance, RectIntConverter.Instance, BoundsConverter.Instance, BoundsIntConverter.Instance, BitSetConverter.Instance, ImmutableBitSetConverter.Instance, DequeConverterFactory.Instance, CyclicBufferConverterFactory.Instance, SerializableSetConverterFactory.Instance, SerializableDictionaryConverterFactory.Instance, SerializableSortedDictionaryConverterFactory.Instance, }, }; } public static JsonSerializerOptions GetFastPocoJsonOptions() { return new JsonSerializerOptions { IgnoreReadOnlyFields = false, IgnoreReadOnlyProperties = false, ReferenceHandler = null, PropertyNameCaseInsensitive = false, IncludeFields = false, NumberHandling = JsonNumberHandling.Strict, ReadCommentHandling = JsonCommentHandling.Disallow, AllowTrailingCommas = false, // No converters for POCO to minimize overhead }; } static SerializerEncoding() { Encoding = Encoding.UTF8; NormalJsonOptions = GetNormalJsonOptions(); PrettyJsonOptions = GetPrettyJsonOptions(); FastJsonOptions = GetFastJsonOptions(); FastPocoJsonOptions = GetFastPocoJsonOptions(); } } /// /// Selects the wire format used by . /// /// /// Choose a format based on your requirements: /// /// /// /// — Human‑readable and diff‑friendly. Uses System.Text.Json with Unity‑aware /// converters for common types (e.g., Vector2/3/4, Matrix4x4, Color, Type). /// Prefer for save files, configs, and tooling. /// /// /// /// /// — Compact binary with great performance using protobuf‑net. /// Prefer for networking, large payloads, and memory‑sensitive scenarios. /// Requires opt‑in attributes like [ProtoContract]/[ProtoMember] or runtime models. /// /// /// /// /// — .NET BinaryFormatter. Legacy and trusted‑only. Not /// cross‑version/portable and unsafe for untrusted input. Use only for ephemeral/dev data. /// /// /// /// public enum SerializationType { /// Unspecified format; not valid for read/write. [Obsolete("Please use a valid enum value")] None = 0, /// Legacy .NET BinaryFormatter. Trusted/ephemeral data only. [Obsolete( "BinaryFormatter is obsolete and unsafe for untrusted data. " + "Prefer Json or Protobuf for new code." )] SystemBinary = 1, /// protobuf-net compact binary. Best for networking and high-performance. Protobuf = 2, /// System.Text.Json text. Human-readable and diff-friendly. Json = 3, } /// /// Unified serialization helpers for JSON, protobuf‑net, and legacy BinaryFormatter. /// /// /// Highlights /// /// JSON: Uses pooled writers and Unity‑aware converters; supports pretty printing. /// Protobuf: Compact binary via protobuf‑net; supports interface/abstract types via root resolution or . /// Binary: Convenience for legacy only; do not feed untrusted data. /// Minimal allocations with ArrayPool-backed streams to reduce GC pressure. /// /// When to use what /// /// Prefer for save systems, settings, and tools. /// Prefer for networking, large or frequent messages. /// Reserve for trusted legacy scenarios only. /// /// /// /// JSON save/config /// /// var save = new SaveData { Level = 3 }; /// // To string /// string text = Serializer.JsonStringify(save, pretty: true); /// // File IO /// Serializer.WriteToJsonFile(save, "save.json", pretty: true); /// var loaded = Serializer.ReadFromJsonFile<SaveData>("save.json"); /// /// Protobuf networking /// /// [ProtoContract] /// class NetworkMessage { [ProtoMember(1)] public int Id { get; set; } } /// byte[] bytes = Serializer.ProtoSerialize(new NetworkMessage { Id = 42 }); /// NetworkMessage msg = Serializer.ProtoDeserialize<NetworkMessage>(bytes); /// /// Legacy BinaryFormatter (trusted only) /// /// byte[] blob = Serializer.BinarySerialize(obj); /// var roundtrip = Serializer.BinaryDeserialize<SomeType>(blob); /// /// public static class Serializer { /// /// Returns a copy of the package's Normal JSON options. The returned instance is independent /// of internal defaults, so modifying it won't affect global behavior. Cache and reuse the /// returned instance across calls to benefit from System.Text.Json metadata caches. /// public static JsonSerializerOptions CreateNormalJsonOptions() => SerializerEncoding.GetNormalJsonOptions(); /// /// Returns a copy of the package's Pretty (indented) JSON options. /// public static JsonSerializerOptions CreatePrettyJsonOptions() => SerializerEncoding.GetPrettyJsonOptions(); /// /// Returns a copy of the package's Fast JSON options, tuned for hot paths with reduced validation /// and features to minimize allocations and branching. See docs for trade-offs. /// public static JsonSerializerOptions CreateFastJsonOptions() => SerializerEncoding.GetFastJsonOptions(); /// /// Returns a copy of the package's Fast POCO JSON options. /// Strict, minimal, and with no Unity-specific converters. /// Use for pure POCO graphs when you want the fastest possible serialization/deserialization. /// Notes: /// - Case-sensitive property names (faster matching) /// - No comments/trailing commas; strict numbers only /// - IncludeFields = false (prefer properties for performance) /// - Returns a new instance each call; cache and reuse within your app to leverage STJ metadata caches /// public static JsonSerializerOptions CreateFastPocoJsonOptions() => new(SerializerEncoding.FastPocoJsonOptions); // Small protobuf payloads benefit from protobuf-net's MemoryStream fast-path (TryGetBuffer). // Larger payloads see wins from our pooled read-only stream to avoid per-iteration allocations. private const int ProtobufMemoryStreamThreshold = 4096; // bytes // Optional zero-copy path if protobuf-net supports ReadOnlyMemory/ReadOnlySequence overloads private static readonly MethodInfo ProtoDeserializeTypeFromROM; private static readonly MethodInfo ProtoDeserializeTypeFromROS; private static readonly Func< Type, ReadOnlyMemory, object > ProtoDeserializeTypeFromROMFast; private static readonly Func< Type, ReadOnlySequence, object > ProtoDeserializeTypeFromROSFast; static Serializer() { // Initialize protobuf surrogates and any other serialization bootstrapping here // so initialization does not depend on JSON option access. ProtobufUnityModel.EnsureInitialized(); try { MethodInfo[] methods = typeof(ProtoBuf.Serializer).GetMethods( BindingFlags.Public | BindingFlags.Static ); foreach (MethodInfo mi in methods) { if (mi.Name != "Deserialize") { continue; } ParameterInfo[] pars = mi.GetParameters(); if (pars.Length != 2) { continue; } if (pars[0].ParameterType != typeof(Type)) { continue; } Type p1 = pars[1].ParameterType; switch (p1.IsGenericType) { case true when p1.GetGenericTypeDefinition() == typeof(ReadOnlyMemory<>): { Type genArg = p1.GetGenericArguments()[0]; if (genArg == typeof(byte)) { ProtoDeserializeTypeFromROM ??= mi; try { ProtoDeserializeTypeFromROMFast = ReflectionHelpers.GetStaticMethodInvoker< Type, ReadOnlyMemory, object >(mi); } catch { } } break; } case true when p1.GetGenericTypeDefinition() == typeof(ReadOnlySequence<>): { Type genArg = p1.GetGenericArguments()[0]; if (genArg == typeof(byte)) { ProtoDeserializeTypeFromROS ??= mi; try { ProtoDeserializeTypeFromROSFast = ReflectionHelpers.GetStaticMethodInvoker< Type, ReadOnlySequence, object >(mi); } catch { } } break; } } } } catch { // Reflection probing failed; keep nulls and fall back to streams } } private static readonly ConcurrentDictionary ProtobufRootCache = new(); private static readonly Type NoRootMarker = typeof(void); // Centralized decision logic for protobuf runtime vs declared handling internal static bool ShouldUseRuntimeTypeForProtobuf( Type declared, T instance, bool forceRuntimeType ) { if (forceRuntimeType) { return true; } if (declared == null) { return true; } if (declared.IsInterface || declared.IsAbstract || declared == typeof(object)) { return true; } // Last resort: if the declared type is a reference type and the runtime type differs, // prefer using the runtime serializer to avoid protobuf-net subtype errors. if (!declared.IsValueType && instance != null && instance.GetType() != declared) { return true; } return false; } /// /// Checks if the type is a serializable collection type that needs wrapper-based protobuf serialization. /// Returns true for SerializableHashSet, SerializableSortedSet, SerializableDictionary, SerializableSortedDictionary. /// private static bool IsSerializableCollectionType(Type type) { if (type == null || !type.IsGenericType) { return false; } Type genericDef = type.GetGenericTypeDefinition(); return genericDef == typeof(SerializableHashSet<>) || genericDef == typeof(SerializableSortedSet<>) || genericDef == typeof(SerializableDictionary<,>) || genericDef == typeof(SerializableSortedDictionary<,>); } /// /// Cached reflection accessors for protobuf collection wrapper serialization. /// Uses ReflectionHelpers for cached delegate generation and nameof() for compile-time safety. /// private static class CollectionProtoAccessors { // Field names using nameof() for compile-time safety via internal access internal const string ItemsFieldName = SerializableHashSetSerializedPropertyNames.Items; internal const string KeysFieldName = SerializableDictionarySerializedPropertyNames.Keys; internal const string ValuesFieldName = SerializableDictionarySerializedPropertyNames.Values; // Use nameof() directly for fields accessible within this assembly internal const string PreserveSerializedEntriesFieldName = nameof( SerializableHashSet._preserveSerializedEntries ); internal const string OnBeforeSerializeMethodName = nameof( SerializableHashSet.OnBeforeSerialize ); internal const string OnAfterDeserializeMethodName = nameof( SerializableHashSet.OnAfterDeserialize ); // Wrapper field names (public fields, nameof() safe) internal const string WrapperItemsFieldName = nameof( SerializableHashSetProtoWrapper.Items ); internal const string WrapperKeysFieldName = nameof( SerializableDictionaryProtoWrapper.Keys ); internal const string WrapperValuesFieldName = nameof( SerializableDictionaryProtoWrapper.Values ); // Binding flags for field/method lookup private const BindingFlags InstanceFieldFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy; private const BindingFlags InstanceMethodFlags = BindingFlags.Public | BindingFlags.Instance; // Cached accessors per closed generic type private static readonly ConcurrentDictionary< Type, ( Func GetItems, Action SetItems, Func GetKeys, Action SetKeys, Func GetValues, Action SetValues, Action SetPreserve, Action OnBeforeSerialize, Action OnAfterDeserialize ) > TypeAccessors = new(); /// /// Gets or creates cached accessors for the specified collection type. /// internal static ( Func GetItems, Action SetItems, Func GetKeys, Action SetKeys, Func GetValues, Action SetValues, Action SetPreserve, Action OnBeforeSerialize, Action OnAfterDeserialize ) GetAccessors(Type collectionType) { return TypeAccessors.GetOrAdd(collectionType, CreateAccessors); } private static ( Func GetItems, Action SetItems, Func GetKeys, Action SetKeys, Func GetValues, Action SetValues, Action SetPreserve, Action OnBeforeSerialize, Action OnAfterDeserialize ) CreateAccessors(Type type) { Type genericDef = type.GetGenericTypeDefinition(); bool isSet = genericDef == typeof(SerializableHashSet<>) || genericDef == typeof(SerializableSortedSet<>); // Items field (for sets) Func getItems = null; Action setItems = null; if (isSet) { FieldInfo itemsField = type.GetField(ItemsFieldName, InstanceFieldFlags); if (itemsField != null) { getItems = ReflectionHelpers.GetFieldGetter(itemsField); setItems = ReflectionHelpers.GetFieldSetter(itemsField); } } // Keys/Values fields (for dictionaries) Func getKeys = null; Action setKeys = null; Func getValues = null; Action setValues = null; if (!isSet) { FieldInfo keysField = type.GetField(KeysFieldName, InstanceFieldFlags); FieldInfo valuesField = type.GetField(ValuesFieldName, InstanceFieldFlags); if (keysField != null) { getKeys = ReflectionHelpers.GetFieldGetter(keysField); setKeys = ReflectionHelpers.GetFieldSetter(keysField); } if (valuesField != null) { getValues = ReflectionHelpers.GetFieldGetter(valuesField); setValues = ReflectionHelpers.GetFieldSetter(valuesField); } } // PreserveSerializedEntries field Action setPreserve = null; FieldInfo preserveField = type.GetField( PreserveSerializedEntriesFieldName, InstanceFieldFlags ); if (preserveField != null) { setPreserve = ReflectionHelpers.GetFieldSetter(preserveField); } // Lifecycle methods Action onBeforeSerialize = null; Action onAfterDeserialize = null; MethodInfo beforeMethod = type.GetMethod( OnBeforeSerializeMethodName, InstanceMethodFlags ); if (beforeMethod != null) { onBeforeSerialize = obj => beforeMethod.Invoke(obj, null); } MethodInfo afterMethod = type.GetMethod( OnAfterDeserializeMethodName, InstanceMethodFlags ); if (afterMethod != null) { onAfterDeserialize = obj => afterMethod.Invoke(obj, null); } return ( getItems, setItems, getKeys, setKeys, getValues, setValues, setPreserve, onBeforeSerialize, onAfterDeserialize ); } /// /// Gets cached accessors for protobuf wrapper types. /// private static readonly ConcurrentDictionary< Type, ( Func GetItems, Action SetItems, Func GetKeys, Action SetKeys, Func GetValues, Action SetValues ) > WrapperAccessors = new(); internal static ( Func GetItems, Action SetItems, Func GetKeys, Action SetKeys, Func GetValues, Action SetValues ) GetWrapperAccessors(Type wrapperType, bool isSet) { return WrapperAccessors.GetOrAdd( wrapperType, t => CreateWrapperAccessors(t, isSet) ); } private static ( Func GetItems, Action SetItems, Func GetKeys, Action SetKeys, Func GetValues, Action SetValues ) CreateWrapperAccessors(Type wrapperType, bool isSet) { Func getItems = null; Action setItems = null; Func getKeys = null; Action setKeys = null; Func getValues = null; Action setValues = null; if (isSet) { FieldInfo itemsField = wrapperType.GetField(WrapperItemsFieldName); if (itemsField != null) { getItems = ReflectionHelpers.GetFieldGetter(itemsField); setItems = ReflectionHelpers.GetFieldSetter(itemsField); } } else { FieldInfo keysField = wrapperType.GetField(WrapperKeysFieldName); FieldInfo valuesField = wrapperType.GetField(WrapperValuesFieldName); if (keysField != null) { getKeys = ReflectionHelpers.GetFieldGetter(keysField); setKeys = ReflectionHelpers.GetFieldSetter(keysField); } if (valuesField != null) { getValues = ReflectionHelpers.GetFieldGetter(valuesField); setValues = ReflectionHelpers.GetFieldSetter(valuesField); } } return (getItems, setItems, getKeys, setKeys, getValues, setValues); } } /// /// Serializes a serializable collection to a protobuf wrapper and then to bytes. /// Uses cached reflection accessors for performance. /// private static byte[] SerializeCollectionWithWrapper(T input) { Type type = typeof(T); Type genericDef = type.GetGenericTypeDefinition(); bool isSet = genericDef == typeof(SerializableHashSet<>) || genericDef == typeof(SerializableSortedSet<>); // Get cached accessors for the collection type ( Func getItems, Action _, Func getKeys, Action __, Func getValues, Action ___, Action ____, Action onBeforeSerialize, Action _____ ) = CollectionProtoAccessors.GetAccessors(type); // Determine wrapper type Type wrapperType; if (genericDef == typeof(SerializableHashSet<>)) { wrapperType = typeof(SerializableHashSetProtoWrapper<>).MakeGenericType( type.GetGenericArguments() ); } else if (genericDef == typeof(SerializableSortedSet<>)) { wrapperType = typeof(SerializableSortedSetProtoWrapper<>).MakeGenericType( type.GetGenericArguments() ); } else if (genericDef == typeof(SerializableDictionary<,>)) { wrapperType = typeof(SerializableDictionaryProtoWrapper<,>).MakeGenericType( type.GetGenericArguments() ); } else if (genericDef == typeof(SerializableSortedDictionary<,>)) { wrapperType = typeof(SerializableSortedDictionaryProtoWrapper<,>).MakeGenericType( type.GetGenericArguments() ); } else { throw new InvalidOperationException( $"Type {type} is not a supported serializable collection type." ); } // Get cached wrapper accessors ( Func _______, Action setWrapperItems, Func ________, Action setWrapperKeys, Func _________, Action setWrapperValues ) = CollectionProtoAccessors.GetWrapperAccessors(wrapperType, isSet); // Call OnBeforeSerialize to ensure arrays are populated onBeforeSerialize?.Invoke(input); // Create wrapper and copy data object wrapper = Activator.CreateInstance(wrapperType); if (isSet) { object items = getItems?.Invoke(input); setWrapperItems?.Invoke(wrapper, items); } else { object keys = getKeys?.Invoke(input); object values = getValues?.Invoke(input); setWrapperKeys?.Invoke(wrapper, keys); setWrapperValues?.Invoke(wrapper, values); } // Serialize wrapper using Utils.PooledResource lease = PooledBufferStream.Rent( out PooledBufferStream stream ); ProtoBuf.Serializer.NonGeneric.Serialize(stream, wrapper); byte[] buffer = null; stream.ToArrayExact(ref buffer); return buffer; } /// /// Deserializes a protobuf wrapper and constructs the serializable collection. /// Uses cached reflection accessors for performance. /// private static T DeserializeCollectionFromWrapper(byte[] data) { Type type = typeof(T); Type genericDef = type.GetGenericTypeDefinition(); bool isSet = genericDef == typeof(SerializableHashSet<>) || genericDef == typeof(SerializableSortedSet<>); // Get cached accessors for the collection type ( Func _, Action setItems, Func __, Action setKeys, Func ___, Action setValues, Action setPreserve, Action ____, Action onAfterDeserialize ) = CollectionProtoAccessors.GetAccessors(type); // Determine wrapper type Type wrapperType; if (genericDef == typeof(SerializableHashSet<>)) { wrapperType = typeof(SerializableHashSetProtoWrapper<>).MakeGenericType( type.GetGenericArguments() ); } else if (genericDef == typeof(SerializableSortedSet<>)) { wrapperType = typeof(SerializableSortedSetProtoWrapper<>).MakeGenericType( type.GetGenericArguments() ); } else if (genericDef == typeof(SerializableDictionary<,>)) { wrapperType = typeof(SerializableDictionaryProtoWrapper<,>).MakeGenericType( type.GetGenericArguments() ); } else if (genericDef == typeof(SerializableSortedDictionary<,>)) { wrapperType = typeof(SerializableSortedDictionaryProtoWrapper<,>).MakeGenericType( type.GetGenericArguments() ); } else { throw new InvalidOperationException( $"Type {type} is not a supported serializable collection type." ); } // Get cached wrapper accessors ( Func getWrapperItems, Action _____, Func getWrapperKeys, Action ______, Func getWrapperValues, Action _______ ) = CollectionProtoAccessors.GetWrapperAccessors(wrapperType, isSet); // Deserialize wrapper using MemoryStream ms = new(data, writable: false); object wrapper = ProtoBuf.Serializer.NonGeneric.Deserialize(wrapperType, ms); // Create result and copy data from wrapper object result = Activator.CreateInstance(type); if (isSet) { object items = getWrapperItems?.Invoke(wrapper); setItems?.Invoke(result, items); } else { object keys = getWrapperKeys?.Invoke(wrapper); object values = getWrapperValues?.Invoke(wrapper); setKeys?.Invoke(result, keys); setValues?.Invoke(result, values); } // Set preserve flag to prevent clearing during OnAfterDeserialize setPreserve?.Invoke(result, true); // Call OnAfterDeserialize to populate the backing collection onAfterDeserialize?.Invoke(result); return (T)result; } private static readonly Utils.WallstopGenericPool BinaryFormatterPool = new(() => new BinaryFormatter()); private static readonly Utils.WallstopGenericPool JsonWriterPool = new( () => new Utf8JsonWriter(Stream.Null, new JsonWriterOptions { SkipValidation = true }), onRelease: writer => { writer.Reset(Stream.Null); }, onDisposal: stream => stream.Dispose() ); /// /// Registers a concrete or abstract protobuf root type for a declared interface/abstract/object type. /// The root must be assignable to and annotated with [ProtoContract]. /// Subsequent deserializations to the declared type will use the registered root. /// /// /// Use this when deserializing to an interface/abstract/object and you want deterministic root selection /// instead of relying on reflection inference. /// /// /// /// // Given an interface and concrete implementation /// [ProtoContract] class PlayerJoined : IEvent { [ProtoMember(1)] public string Name { get; set; } } /// Serializer.RegisterProtobufRoot(typeof(IEvent), typeof(PlayerJoined)); /// var evt = Serializer.ProtoDeserialize<IEvent>(bytes); /// /// /// If declared or root is null. /// If root is not assignable to declared or missing [ProtoContract]. /// If a conflicting root is already registered. public static void RegisterProtobufRoot(Type declared, Type root) { if (declared == null) { throw new ArgumentNullException(nameof(declared)); } if (root == null) { throw new ArgumentNullException(nameof(root)); } if (!declared.IsAssignableFrom(root)) { throw new ArgumentException( $"Type {root.FullName} is not assignable to {declared.FullName}", nameof(root) ); } if (!ReflectionHelpers.HasAttributeSafe(root)) { throw new ArgumentException( $"Type {root.FullName} must be annotated with [ProtoContract]", nameof(root) ); } if (ProtobufRootCache.TryGetValue(declared, out Type existing)) { if (existing != root && existing != NoRootMarker) { throw new InvalidOperationException( $"A different root {existing.FullName} is already registered for {declared.FullName}" ); } } ProtobufRootCache[declared] = root; } /// /// Generic convenience overload for registering a protobuf root type. /// /// /// Useful for polymorphic APIs: map to once, /// then call for the declared type. /// /// /// /// Serializer.RegisterProtobufRoot<IEvent, PlayerJoined>(); /// IEvent evt = Serializer.ProtoDeserialize<IEvent>(bytes); /// /// public static void RegisterProtobufRoot() where TRoot : TDeclared { RegisterProtobufRoot(typeof(TDeclared), typeof(TRoot)); } /// /// Deserializes a payload that was serialized with the specified . /// /// The target type. /// Payload bytes to decode. /// The format the payload is encoded with. /// The decoded instance. /// /// JSON /// /// byte[] data = Serializer.JsonSerialize(save); /// SaveData loaded = Serializer.Deserialize<SaveData>(data, SerializationType.Json); /// /// Protobuf /// /// byte[] msg = Serializer.ProtoSerialize(message); /// NetworkMessage decoded = Serializer.Deserialize<NetworkMessage>(msg, SerializationType.Protobuf); /// /// public static T Deserialize(byte[] serialized, SerializationType serializationType) { switch (serializationType) { #pragma warning disable CS0618 // Type or member is obsolete case SerializationType.SystemBinary: #pragma warning restore CS0618 // Type or member is obsolete { return BinaryDeserialize(serialized); } case SerializationType.Protobuf: { return ProtoDeserialize(serialized); } case SerializationType.Json: { return JsonDeserialize(serialized); } default: { throw new InvalidEnumArgumentException( nameof(serializationType), (int)serializationType, typeof(SerializationType) ); } } } /// /// Serializes an instance into bytes using the specified . /// /// The instance type. /// The instance to encode. /// The target wire format. /// Serialized bytes. /// /// /// // As bytes /// byte[] data = Serializer.Serialize(save, SerializationType.Json); /// // Later /// SaveData loaded = Serializer.Deserialize<SaveData>(data, SerializationType.Json); /// /// public static byte[] Serialize(T instance, SerializationType serializationType) { switch (serializationType) { #pragma warning disable CS0618 // Type or member is obsolete case SerializationType.SystemBinary: #pragma warning restore CS0618 // Type or member is obsolete { return BinarySerialize(instance); } case SerializationType.Protobuf: { return ProtoSerialize(instance); } case SerializationType.Json: { return JsonSerialize(instance); } default: { throw new InvalidEnumArgumentException( nameof(serializationType), (int)serializationType, typeof(SerializationType) ); } } } /// /// Serializes into a caller-provided buffer to avoid an extra allocation. /// /// The instance type. /// The instance to encode. /// The target wire format. /// Destination buffer reference. Resized if too small. /// The number of valid bytes written to . public static int Serialize( T instance, SerializationType serializationType, ref byte[] buffer ) { switch (serializationType) { #pragma warning disable CS0618 // Type or member is obsolete case SerializationType.SystemBinary: #pragma warning restore CS0618 // Type or member is obsolete { return BinarySerialize(instance, ref buffer); } case SerializationType.Protobuf: { return ProtoSerialize(instance, ref buffer); } case SerializationType.Json: { return JsonSerialize(instance, ref buffer); } default: { throw new InvalidEnumArgumentException( nameof(serializationType), (int)serializationType, typeof(SerializationType) ); } } } /// /// Deserializes bytes using legacy BinaryFormatter. /// /// Target type. /// Serialized bytes. /// /// Security: Never deserialize untrusted data with BinaryFormatter. It is obsolete and unsafe. /// Portability: Fragile across versions/platforms; avoid for long‑lived data. /// Prefer or in production. /// public static T BinaryDeserialize(byte[] data) { using Utils.PooledResource lease = PooledReadOnlyMemoryStream.Rent(out PooledReadOnlyMemoryStream stream); stream.SetBuffer(data); using Utils.PooledResource fmtLease = BinaryFormatterPool.Get( out BinaryFormatter binaryFormatter ); return (T)binaryFormatter.Deserialize(stream); } /// /// Serializes an object using legacy BinaryFormatter. /// /// Instance type. /// Object to serialize. /// Serialized bytes. /// /// Use for trusted, temporary data only. Not safe for untrusted input. Prefer JSON or protobuf. /// public static byte[] BinarySerialize(T input) { using Utils.PooledResource lease = PooledBufferStream.Rent( out PooledBufferStream stream ); using Utils.PooledResource fmtLease = BinaryFormatterPool.Get( out BinaryFormatter binaryFormatter ); binaryFormatter.Serialize(stream, input); byte[] buffer = null; stream.ToArrayExact(ref buffer); return buffer; } /// /// Serializes to a caller buffer using BinaryFormatter. /// /// Instance type. /// Object to serialize. /// Destination buffer reference. Resized if necessary. /// Number of bytes written. public static int BinarySerialize(T input, ref byte[] buffer) { using Utils.PooledResource lease = PooledBufferStream.Rent( out PooledBufferStream stream ); using Utils.PooledResource fmtLease = BinaryFormatterPool.Get( out BinaryFormatter binaryFormatter ); binaryFormatter.Serialize(stream, input); return stream.ToArrayExact(ref buffer); } /// /// Deserializes protobuf‑net bytes to . /// /// Target type. /// Encoded protobuf payload. /// The decoded instance. /// /// Polymorphism and interfaces: /// - If is an interface, abstract type, or , deserialization /// requires a concrete root type. We resolve this by either using an abstract base that is marked with /// [ProtoContract] and [ProtoInclude] for all subtypes (e.g., /// AbstractRandom in the random package) or by a previously registered mapping via /// . If no unique root is found, a /// is thrown to avoid ambiguous heuristics. /// /// Examples /// (bytes); /// /// // 2) Using an interface by registering a root /// interface IEvent { } /// [ProtoContract] class PlayerJoined : IEvent { [ProtoMember(1)] public string Name { get; set; } } /// Serializer.RegisterProtobufRoot(); /// IEvent evt = Serializer.ProtoDeserialize(bytes); /// /// // 3) Overload that specifies the concrete type explicitly /// IEvent evt2 = Serializer.ProtoDeserialize(bytes, typeof(PlayerJoined)); /// ]]> /// public static T ProtoDeserialize(byte[] data) { if (data == null) { throw new ProtoException("No data provided for Protobuf deserialization."); } // Intercept serializable collection types to use wrapper-based deserialization // This bypasses protobuf-net's collection detection which ignores IgnoreListHandling Type declared = typeof(T); if (IsSerializableCollectionType(declared)) { return DeserializeCollectionFromWrapper(data); } try { // Prefer zero-copy ROM/ROS overloads when available if (ProtoDeserializeTypeFromROMFast != null) { ReadOnlyMemory rom = new(data); if ( ShouldUseRuntimeTypeForProtobuf( declared, default, forceRuntimeType: false ) ) { Type root = ResolveProtobufRootType(declared); if (root != null) { return (T)ProtoDeserializeTypeFromROMFast(root, rom); } throw new ProtoException( $"Unable to resolve a unique protobuf root for declared type {declared.FullName}. Register a root via RegisterProtobufRoot or annotate a shared abstract base with [ProtoInclude]s." ); } return (T)ProtoDeserializeTypeFromROMFast(declared, rom); } if (ProtoDeserializeTypeFromROSFast != null) { ReadOnlySequence ros = new(data); if ( ShouldUseRuntimeTypeForProtobuf( declared, default, forceRuntimeType: false ) ) { Type root = ResolveProtobufRootType(declared); if (root != null) { return (T)ProtoDeserializeTypeFromROSFast(root, ros); } throw new ProtoException( $"Unable to resolve a unique protobuf root for declared type {declared.FullName}. Register a root via RegisterProtobufRoot or annotate a shared abstract base with [ProtoInclude]s." ); } return (T)ProtoDeserializeTypeFromROSFast(declared, ros); } // For small payloads, allow protobuf-net to use MemoryStream's non-copy buffer access if (data.Length <= ProtobufMemoryStreamThreshold) { using MemoryStream ms = new(data, writable: false); if ( ShouldUseRuntimeTypeForProtobuf( declared, default, forceRuntimeType: false ) ) { Type root = ResolveProtobufRootType(declared); if (root != null) { return (T)ProtoBuf.Serializer.Deserialize(root, ms); } throw new ProtoException( $"Unable to resolve a unique protobuf root for declared type {declared.FullName}. Register a root via RegisterProtobufRoot or annotate a shared abstract base with [ProtoInclude]s." ); } return ProtoBuf.Serializer.Deserialize(ms); } // For larger payloads, prefer pooled stream to avoid per-iteration allocations using Utils.PooledResource lease = PooledReadOnlyMemoryStream.Rent(out PooledReadOnlyMemoryStream stream); stream.SetBuffer(data); Type declaredLarge = typeof(T); if ( ShouldUseRuntimeTypeForProtobuf( declaredLarge, default, forceRuntimeType: false ) ) { Type root = ResolveProtobufRootType(declaredLarge); if (root != null) { return (T)ProtoBuf.Serializer.Deserialize(root, stream); } throw new ProtoException( $"Unable to resolve a unique protobuf root for declared type {declaredLarge.FullName}. Register a root via RegisterProtobufRoot or annotate a shared abstract base with [ProtoInclude]s." ); } return ProtoBuf.Serializer.Deserialize(stream); } catch (ProtoException) { throw; } catch (Exception e) { throw new ProtoException( "Protobuf deserialization failed: invalid or corrupted data.", e ); } } // Attempts to resolve a concrete root type for protobuf-net when the declared generic type // is interface/abstract/object. // Rules: // - If a root is explicitly registered, use it. // - If the declared type itself is an abstract [ProtoContract] (with [ProtoInclude]s), return the declared type // to allow protobuf-net to handle subtypes via includes. // - Do not auto-pick implementations based on reflection heuristics; require registration instead to avoid // ambiguity and brittle ordering of loaded types. private static Type ResolveProtobufRootType(Type declared) { if (declared == null) { return null; } // If declared is already a usable concrete type, just return it if (!declared.IsInterface && !declared.IsAbstract && declared != typeof(object)) { return declared; } // If declared itself is an abstract [ProtoContract] base with [ProtoInclude]s, prefer it if ( declared.IsAbstract && ReflectionHelpers.HasAttributeSafe(declared) ) { return declared; } if (ProtobufRootCache.TryGetValue(declared, out Type cached)) { return cached == NoRootMarker ? null : cached; } // Try to resolve a unique abstract [ProtoContract] base that implements the declared interface. // This allows scenarios like: IRandom -> AbstractRandom (annotated with [ProtoContract] + [ProtoInclude]). // We deliberately keep the search local to the declaring assembly to avoid brittle cross-assembly heuristics. if (declared.IsInterface && declared != typeof(object)) { try { Type[] types = ReflectionHelpers.GetTypesFromAssembly(declared.Assembly); using PooledResource> candidatesLease = Buffers.List.Get( out List candidates ); for (int i = 0; i < types.Length; i++) { Type t = types[i]; if ( t.IsClass && t.IsAbstract && declared.IsAssignableFrom(t) && ReflectionHelpers.HasAttributeSafe(t) ) { candidates.Add(t); } } switch (candidates.Count) { case 1: { Type root = candidates[0]; ProtobufRootCache[declared] = root; return root; } case > 1: { // Prefer a candidate that explicitly declares [ProtoInclude]s if this disambiguates using PooledResource> includeCandidatesLease = Buffers.List.Get(out List includeCandidates); for (int i = 0; i < candidates.Count; i++) { Type t = candidates[i]; if (ReflectionHelpers.HasAttributeSafe(t)) { includeCandidates.Add(t); } } if (includeCandidates.Count == 1) { Type root = includeCandidates[0]; ProtobufRootCache[declared] = root; return root; } break; } } } catch { // Reflection may fail in some restricted environments; fall through to marker/null } } ProtobufRootCache[declared] = NoRootMarker; return null; } /// /// Deserializes protobuf‑net bytes into the provided . /// /// Expected return type after cast. /// Encoded protobuf payload. /// Concrete type to deserialize to. /// The decoded instance cast to . public static T ProtoDeserialize(byte[] data, Type type) { if (data == null) { throw new ArgumentException(nameof(data)); } if (type == null) { throw new ArgumentNullException(nameof(type)); } try { // Prefer zero-copy ROM/ROS overloads when available if (ProtoDeserializeTypeFromROMFast != null) { ReadOnlyMemory rom = new(data); return (T)ProtoDeserializeTypeFromROMFast(type, rom); } if (ProtoDeserializeTypeFromROSFast != null) { ReadOnlySequence ros = new(data); return (T)ProtoDeserializeTypeFromROSFast(type, ros); } if (data.Length <= ProtobufMemoryStreamThreshold) { using MemoryStream ms = new(data, writable: false); return (T)ProtoBuf.Serializer.Deserialize(type, ms); } using Utils.PooledResource lease = PooledReadOnlyMemoryStream.Rent(out PooledReadOnlyMemoryStream stream); stream.SetBuffer(data); return (T)ProtoBuf.Serializer.Deserialize(type, stream); } catch (ProtoException) { throw; } catch (Exception e) { throw new ProtoException( "Protobuf deserialization failed: invalid or corrupted data.", e ); } } /// /// Serializes an instance to protobuf‑net bytes. /// /// Declared type. /// The instance to serialize. /// When true, always serialize as the runtime type; otherwise uses declared type unless it is interface/abstract/object. /// Serialized bytes. /// /// /// [ProtoContract] /// class NetworkMessage { [ProtoMember(1)] public int Id { get; set; } } /// var bytes = Serializer.ProtoSerialize(new NetworkMessage { Id = 5 }); /// var msg = Serializer.ProtoDeserialize<NetworkMessage>(bytes); /// /// public static byte[] ProtoSerialize(T input, bool forceRuntimeType = false) { Type declared = typeof(T); // Intercept serializable collection types to use wrapper-based serialization // This bypasses protobuf-net's collection detection which ignores IgnoreListHandling if (IsSerializableCollectionType(declared)) { return SerializeCollectionWithWrapper(input); } using Utils.PooledResource lease = PooledBufferStream.Rent( out PooledBufferStream stream ); bool useRuntime = ShouldUseRuntimeTypeForProtobuf(declared, input, forceRuntimeType); if (useRuntime) { ProtoBuf.Serializer.NonGeneric.Serialize(stream, input); } else { ProtoBuf.Serializer.Serialize(stream, input); } byte[] buffer = null; stream.ToArrayExact(ref buffer); return buffer; } /// /// Serializes an instance to protobuf‑net bytes into a caller-provided buffer. /// /// Declared type. /// The instance to serialize. /// Destination buffer reference. Resized if necessary. /// When true, always serialize as the runtime type. /// Number of bytes written. public static int ProtoSerialize( T input, ref byte[] buffer, bool forceRuntimeType = false ) { Type declared = typeof(T); // Intercept serializable collection types to use wrapper-based serialization if (IsSerializableCollectionType(declared)) { byte[] result = SerializeCollectionWithWrapper(input); if (buffer == null || buffer.Length < result.Length) { buffer = new byte[result.Length]; } Array.Copy(result, buffer, result.Length); return result.Length; } using Utils.PooledResource lease = PooledBufferStream.Rent( out PooledBufferStream stream ); bool useRuntime = ShouldUseRuntimeTypeForProtobuf(declared, input, forceRuntimeType); if (useRuntime) { ProtoBuf.Serializer.NonGeneric.Serialize(stream, input); } else { ProtoBuf.Serializer.Serialize(stream, input); } return stream.ToArrayExact(ref buffer); } /// /// Deserializes JSON text to using Unity‑aware converters. /// /// Target type. /// JSON string. /// Optional concrete target type (defaults to ). /// Serializer options; defaults include converters for Unity types and ReferenceHandler.IgnoreCycles. /// The decoded instance. public static T JsonDeserialize( string data, Type type = null, JsonSerializerOptions options = null ) { return (T) JsonSerializer.Deserialize( data, type ?? typeof(T), options ?? SerializerEncoding.NormalJsonOptions ); } /// /// Deserializes JSON bytes (UTF-8) to using Unity-aware converters. /// Avoids intermediate string allocation by using span-based System.Text.Json APIs. /// /// Target type. /// UTF-8 JSON bytes. /// Optional concrete target type (defaults to ). /// Serializer options; defaults include Unity converters. /// The decoded instance. public static T JsonDeserialize( byte[] data, Type type = null, JsonSerializerOptions options = null ) { if (data == null) { throw new ArgumentNullException(nameof(data)); } ReadOnlySpan span = new(data); return (T) JsonSerializer.Deserialize( span, type ?? typeof(T), options ?? SerializerEncoding.NormalJsonOptions ); } /// /// Fast-path JSON deserialize using strict, allocation-lean options. /// public static T JsonDeserializeFast(byte[] data) { if (data == null) { throw new ArgumentNullException(nameof(data)); } ReadOnlySpan span = new(data); return JsonSerializer.Deserialize(span, SerializerEncoding.FastJsonOptions); } /// /// Serializes an instance to JSON bytes (UTF‑8) using Unity‑aware converters. /// /// Instance type. /// The instance to serialize. /// UTF‑8 JSON bytes. public static byte[] JsonSerialize(T input) { using Utils.PooledResource lease = PooledArrayBufferWriter.Rent(out PooledArrayBufferWriter bufferWriter); WriteJsonToBuffer(input, SerializerEncoding.NormalJsonOptions, bufferWriter); byte[] buffer = null; bufferWriter.ToArrayExact(ref buffer); return buffer; } /// /// Serializes an instance to JSON bytes (UTF-8) using caller-provided options. /// Tip: Reuse the same options instance across calls to benefit from metadata caches. /// public static byte[] JsonSerialize(T input, JsonSerializerOptions options) { using Utils.PooledResource lease = PooledArrayBufferWriter.Rent(out PooledArrayBufferWriter bufferWriter); WriteJsonToBuffer(input, options ?? SerializerEncoding.NormalJsonOptions, bufferWriter); byte[] buffer = null; bufferWriter.ToArrayExact(ref buffer); return buffer; } /// /// Serializes an instance to JSON bytes (UTF‑8) into a caller-provided buffer. /// /// Instance type. /// The instance to serialize. /// Destination buffer reference. Resized if necessary. /// Number of bytes written. public static int JsonSerialize(T input, ref byte[] buffer) { using Utils.PooledResource lease = PooledArrayBufferWriter.Rent(out PooledArrayBufferWriter bufferWriter); WriteJsonToBuffer(input, SerializerEncoding.NormalJsonOptions, bufferWriter); return bufferWriter.ToArrayExact(ref buffer); } /// /// Serializes into a caller-provided buffer using caller options. /// Reuses the provided buffer when large enough to avoid allocations; resizes if necessary. /// public static int JsonSerialize( T input, JsonSerializerOptions options, ref byte[] buffer ) { using Utils.PooledResource lease = PooledArrayBufferWriter.Rent(out PooledArrayBufferWriter bufferWriter); WriteJsonToBuffer(input, options ?? SerializerEncoding.NormalJsonOptions, bufferWriter); return bufferWriter.ToArrayExact(ref buffer); } /// /// Serializes into a caller-provided buffer using caller options and a size hint to reduce growth copies. /// Provide an approximate size of the final payload to minimize buffer growth/copy churn for large outputs. /// Example: for large int[] payloads, estimate (count * 12) + overhead. /// public static int JsonSerialize( T input, JsonSerializerOptions options, int sizeHint, ref byte[] buffer ) { using Utils.PooledResource lease = PooledArrayBufferWriter.Rent(out PooledArrayBufferWriter bufferWriter); if (sizeHint > 0) { bufferWriter.Preallocate(sizeHint); } WriteJsonToBuffer(input, options ?? SerializerEncoding.NormalJsonOptions, bufferWriter); return bufferWriter.ToArrayExact(ref buffer); } /// /// Fast-path JSON serialize using strict, allocation-lean options. /// public static byte[] JsonSerializeFast(T input) { using Utils.PooledResource lease = PooledArrayBufferWriter.Rent(out PooledArrayBufferWriter bufferWriter); WriteJsonToBuffer(input, SerializerEncoding.FastJsonOptions, bufferWriter); byte[] buffer = null; bufferWriter.ToArrayExact(ref buffer); return buffer; } /// /// Fast-path JSON serialize into a caller-provided buffer. /// public static int JsonSerializeFast(T input, ref byte[] buffer) { using Utils.PooledResource lease = PooledArrayBufferWriter.Rent(out PooledArrayBufferWriter bufferWriter); WriteJsonToBuffer(input, SerializerEncoding.FastJsonOptions, bufferWriter); return bufferWriter.ToArrayExact(ref buffer); } private static void WriteJsonToStream( T input, JsonSerializerOptions options, Stream stream ) { if (options == null) { throw new ArgumentNullException(nameof(options)); } if (stream == null) { throw new ArgumentNullException(nameof(stream)); } using (JsonWriterPool.Get(out Utf8JsonWriter writer)) { writer.Reset(stream); Type parameterType = typeof(T); if ( parameterType.IsAbstract || parameterType.IsInterface || parameterType == typeof(object) ) { object data = input; if (data == null) { writer.WriteStartObject(); writer.WriteEndObject(); writer.Flush(); return; } Type type = data.GetType(); JsonSerializer.Serialize(writer, data, type, options); } else { JsonSerializer.Serialize(writer, input, options); } writer.Flush(); } } /// /// Serializes an instance to a JSON string. /// /// Instance type. /// The instance to serialize. /// Write indented output when true. /// JSON text. /// /// /// var json = Serializer.JsonStringify(save, pretty: true); /// var roundtrip = Serializer.JsonDeserialize<SaveData>(json); /// /// public static string JsonStringify(T input, bool pretty = false) { JsonSerializerOptions options = pretty ? SerializerEncoding.PrettyJsonOptions : SerializerEncoding.NormalJsonOptions; return JsonStringify(input, options); } /// /// Serializes an instance to a JSON string using the provided . /// /// Instance type. /// The instance to serialize. /// Serializer options. /// JSON text. public static string JsonStringify(T input, JsonSerializerOptions options) { if (options == null) { throw new ArgumentNullException(nameof(options)); } Type parameterType = typeof(T); if ( parameterType.IsAbstract || parameterType.IsInterface || parameterType == typeof(object) ) { object data = input; if (data == null) { return "{}"; } Type type = data.GetType(); return JsonSerializer.Serialize(data, type, options); } return JsonSerializer.Serialize(input, options); } /// /// Reads JSON text from a file (UTF‑8) and deserializes to . /// /// Target type. /// File path. /// Decoded instance. public static T ReadFromJsonFile(string path) { byte[] settingsAsBytes = File.ReadAllBytes(path); return JsonDeserialize(settingsAsBytes); } private static void WriteJsonToBuffer( T input, JsonSerializerOptions options, PooledArrayBufferWriter buffer ) { if (options == null) { throw new ArgumentNullException(nameof(options)); } if (buffer == null) { throw new ArgumentNullException(nameof(buffer)); } using ( Utf8JsonWriter writer = new(buffer, new JsonWriterOptions { SkipValidation = true }) ) { Type parameterType = typeof(T); if ( parameterType.IsAbstract || parameterType.IsInterface || parameterType == typeof(object) ) { object data = input; if (data == null) { writer.WriteStartObject(); writer.WriteEndObject(); writer.Flush(); return; } Type type = data.GetType(); JsonSerializer.Serialize(writer, data, type, options); } else { JsonSerializer.Serialize(writer, input, options); } writer.Flush(); } } /// /// Asynchronously reads JSON text from a file (UTF‑8) and deserializes to . /// /// Target type. /// File path. /// Decoded instance. public static async Task ReadFromJsonFileAsync(string path) { byte[] settingsAsBytes = await File.ReadAllBytesAsync(path); return JsonDeserialize(settingsAsBytes); } /// /// Asynchronously reads JSON with cancellation. /// public static async Task ReadFromJsonFileAsync( string path, System.Threading.CancellationToken cancellationToken ) { using FileStream fs = new( path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true ); using Utils.PooledResource lease = PooledBufferStream.Rent( out PooledBufferStream stream ); byte[] buffer = new byte[8192]; int read; while ((read = await fs.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) { stream.Write(buffer, 0, read); } ArraySegment seg = stream.GetWrittenSegment(); return JsonDeserialize(seg.Array.AsSpan(0, seg.Count).ToArray()); } /// /// Writes an instance to a JSON file (UTF‑8). /// /// Instance type. /// The instance to serialize. /// Destination file path. /// Write indented output when true. public static void WriteToJsonFile(T input, string path, bool pretty = true) { string jsonAsText = JsonStringify(input, pretty); File.WriteAllText(path, jsonAsText); } /// /// Asynchronously writes an instance to a JSON file (UTF‑8). /// /// Instance type. /// The instance to serialize. /// Destination file path. /// Write indented output when true. public static async Task WriteToJsonFileAsync(T input, string path, bool pretty = true) { string jsonAsText = JsonStringify(input, pretty); await File.WriteAllTextAsync(path, jsonAsText); } /// /// Asynchronously writes an instance to a JSON file (UTF‑8) with cancellation. /// public static async Task WriteToJsonFileAsync( T input, string path, bool pretty, System.Threading.CancellationToken cancellationToken ) { string jsonAsText = JsonStringify(input, pretty); byte[] bytes = SerializerEncoding.Encoding.GetBytes(jsonAsText); using FileStream fs = new( path, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true ); await fs.WriteAsync(bytes, 0, bytes.Length, cancellationToken); } /// /// Writes an instance to a JSON file (UTF‑8) using the provided . /// /// Instance type. /// The instance to serialize. /// Destination file path. /// Serializer options. public static void WriteToJsonFile(T input, string path, JsonSerializerOptions options) { string jsonAsText = JsonStringify(input, options); File.WriteAllText(path, jsonAsText); } /// /// Asynchronously writes an instance to a JSON file (UTF‑8) using the provided . /// /// Instance type. /// The instance to serialize. /// Destination file path. /// Serializer options. public static async Task WriteToJsonFileAsync( T input, string path, JsonSerializerOptions options ) { string jsonAsText = JsonStringify(input, options); await File.WriteAllTextAsync(path, jsonAsText); } /// /// Attempts to read JSON into an instance, returns false if file missing or invalid. /// public static bool TryReadFromJsonFile(string path, out T value) { try { if (!File.Exists(path)) { value = default; return false; } string json = File.ReadAllText(path); value = JsonDeserialize(json); return true; } catch { value = default; return false; } } /// /// Attempts to write JSON to a file, returns false on failure. /// public static bool TryWriteToJsonFile(T input, string path, bool pretty = true) { try { WriteToJsonFile(input, path, pretty); return true; } catch { return false; } } } // Internal pooled, growable write stream backed by ArrayPool to reduce allocations internal sealed class PooledBufferStream : Stream { private const int DefaultInitialCapacity = 256; private byte[] _buffer; private int _length; private int _position; private bool _disposed; private static readonly Utils.WallstopGenericPool Pool = new( producer: () => new PooledBufferStream(), onRelease: stream => stream.ResetForReuse(), onDisposal: stream => stream.Dispose() ); public static Utils.PooledResource Rent( out PooledBufferStream stream ) => Pool.Get(out stream); private PooledBufferStream(int initialCapacity = DefaultInitialCapacity) { if (initialCapacity < 1) { initialCapacity = DefaultInitialCapacity; } _buffer = ArrayPool.Shared.Rent(initialCapacity); _length = 0; _position = 0; } internal ArraySegment GetWrittenSegment() { return new ArraySegment(_buffer, 0, _length); } private void ResetForReuse() { _length = 0; _position = 0; _disposed = false; } public override bool CanRead => false; public override bool CanSeek => true; public override bool CanWrite => true; public override long Length => _length; public override long Position { get => _position; set => Seek(value, SeekOrigin.Begin); } public override void Flush() { } public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } public override long Seek(long offset, SeekOrigin origin) { int basePos = origin switch { SeekOrigin.Begin => 0, SeekOrigin.Current => _position, SeekOrigin.End => _length, _ => 0, }; long newPos = basePos + offset; if (newPos is < 0 or > int.MaxValue) { throw new ArgumentOutOfRangeException(nameof(offset)); } _position = (int)newPos; return _position; } public override void SetLength(long value) { if (value is < 0 or > int.MaxValue) { throw new ArgumentOutOfRangeException(nameof(value)); } int newLen = (int)value; EnsureCapacity(newLen); _length = newLen; if (_position > _length) { _position = _length; } } public override void Write(byte[] buffer, int offset, int count) { int endPos = _position + count; EnsureCapacity(endPos); Array.Copy(buffer, offset, _buffer, _position, count); _position = endPos; if (endPos > _length) { _length = endPos; } } public override void WriteByte(byte value) { int endPos = _position + 1; EnsureCapacity(endPos); _buffer[_position] = value; _position = endPos; if (endPos > _length) { _length = endPos; } } private void EnsureCapacity(int required) { if (_buffer.Length >= required) { return; } int newSize = _buffer.Length; if (newSize < DefaultInitialCapacity) { newSize = DefaultInitialCapacity; } while (newSize < required) { newSize = newSize < 1024 ? newSize * 2 : newSize + (newSize >> 1); } byte[] newBuf = ArrayPool.Shared.Rent(newSize); if (_length > 0) { Array.Copy(_buffer, newBuf, _length); } ArrayPool.Shared.Return(_buffer); _buffer = newBuf; } protected override void Dispose(bool disposing) { if (!_disposed) { if (_buffer != null) { ArrayPool.Shared.Return(_buffer); _buffer = Array.Empty(); } _length = 0; _position = 0; _disposed = true; } base.Dispose(disposing); } public int ToArrayExact(ref byte[] buffer) { if (buffer == null || buffer.Length < _length) { buffer = new byte[_length]; } if (_length > 0) { Array.Copy(_buffer, buffer, _length); } return _length; } public override void Write(ReadOnlySpan buffer) { int count = buffer.Length; int endPos = _position + count; EnsureCapacity(endPos); buffer.CopyTo(new Span(_buffer, _position, count)); _position = endPos; if (endPos > _length) { _length = endPos; } } public override ValueTask WriteAsync( ReadOnlyMemory source, System.Threading.CancellationToken cancellationToken = default ) { // Delegate to synchronous span-based path; callers expect a fast in-memory stream Write(source.Span); return new ValueTask(); } } // Internal pooled ArrayBufferWriter-like implementation to enable zero-copy JSON writing via IBufferWriter internal sealed class PooledArrayBufferWriter : IBufferWriter, IDisposable { private const int DefaultInitialCapacity = 256; private byte[] _buffer; private int _written; private bool _disposed; private static readonly Utils.WallstopGenericPool Pool = new( producer: () => new PooledArrayBufferWriter(), onRelease: w => { w.Reset(); } ); public static Utils.PooledResource Rent( out PooledArrayBufferWriter writer ) => Pool.Get(out writer); private PooledArrayBufferWriter(int initialCapacity = DefaultInitialCapacity) { _buffer = ArrayPool.Shared.Rent(initialCapacity); _written = 0; } private void EnsureCapacity(int sizeHint) { if (sizeHint <= 0) { sizeHint = 1; } int required = _written + sizeHint; if (_buffer.Length >= required) { return; } int newSize = _buffer.Length; while (newSize < required) { newSize = newSize < 1024 ? newSize * 2 : newSize + (newSize >> 1); } byte[] newBuf = ArrayPool.Shared.Rent(newSize); if (_written > 0) { Buffer.BlockCopy(_buffer, 0, newBuf, 0, _written); } ArrayPool.Shared.Return(_buffer); _buffer = newBuf; } public void Advance(int count) { _written += count; } public Memory GetMemory(int sizeHint = 0) { EnsureCapacity(sizeHint); return _buffer.AsMemory(_written); } public Span GetSpan(int sizeHint = 0) { EnsureCapacity(sizeHint); return _buffer.AsSpan(_written); } public int WrittenCount => _written; public void Preallocate(int sizeHint) { EnsureCapacity(sizeHint); } public int ToArrayExact(ref byte[] buffer) { if (buffer == null || buffer.Length < _written) { buffer = new byte[_written]; } if (_written > 0) { Buffer.BlockCopy(_buffer, 0, buffer, 0, _written); } return _written; } private void Reset() { // Keep the rented buffer to avoid churn; just reset write cursor. if (_buffer == null || _buffer.Length == 0) { _buffer = ArrayPool.Shared.Rent(DefaultInitialCapacity); } _written = 0; _disposed = false; } public void Dispose() { if (!_disposed) { if (_buffer != null) { ArrayPool.Shared.Return(_buffer); } _buffer = Array.Empty(); _written = 0; _disposed = true; } } } // Internal pooled read-only stream over an existing byte[] to avoid MemoryStream allocation in deserialization paths internal sealed class PooledReadOnlyMemoryStream : Stream { private byte[] _buffer = Array.Empty(); private int _position; private int _length; private static readonly Utils.WallstopGenericPool Pool = new( producer: () => new PooledReadOnlyMemoryStream(), onRelease: s => { s.ResetForReuse(); } ); public static Utils.PooledResource Rent( out PooledReadOnlyMemoryStream stream ) => Pool.Get(out stream); public void SetBuffer(byte[] buffer) { _buffer = buffer ?? Array.Empty(); _position = 0; _length = _buffer.Length; } private void ResetForReuse() { SetBuffer(Array.Empty()); } public override bool CanRead => true; public override bool CanSeek => true; public override bool CanWrite => false; public override long Length => _length; public override long Position { get => _position; set { if (value is < 0 or > int.MaxValue) { throw new ArgumentOutOfRangeException(nameof(value)); } _position = (int)value; } } public override void Flush() { } public override int Read(byte[] buffer, int offset, int count) { if (buffer == null) { throw new ArgumentNullException(nameof(buffer)); } if ((uint)offset > buffer.Length || (uint)count > buffer.Length - offset) { throw new ArgumentOutOfRangeException(); } int remaining = _length - _position; if (remaining <= 0) { return 0; } if (count > remaining) { count = remaining; } Array.Copy(_buffer, _position, buffer, offset, count); _position += count; return count; } // Span-based fast-path used by modern callers (e.g., protobuf-net) public override int Read(Span destination) { int remaining = _length - _position; if (remaining <= 0) { return 0; } int toCopy = destination.Length; if (toCopy > remaining) { toCopy = remaining; } new ReadOnlySpan(_buffer, _position, toCopy).CopyTo(destination); _position += toCopy; return toCopy; } public override ValueTask ReadAsync( Memory destination, System.Threading.CancellationToken cancellationToken = default ) { // Delegate to synchronous span-based path; this stream is purely in-memory int read = Read(destination.Span); return new ValueTask(read); } public override int ReadByte() { if (_position >= _length) { return -1; } return _buffer[_position++]; } public override long Seek(long offset, SeekOrigin origin) { int basePos = origin switch { SeekOrigin.Begin => 0, SeekOrigin.Current => _position, SeekOrigin.End => _length, _ => 0, }; long newPos = basePos + offset; if (newPos is < 0 or > int.MaxValue) { throw new IOException("Attempted to seek outside the stream bounds."); } _position = (int)newPos; return _position; } public override void SetLength(long value) { throw new NotSupportedException(); } public override void Write(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } public override void WriteByte(byte value) { throw new NotSupportedException(); } } }