// 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