// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Serialization.JsonConverters { using System; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using WallstopStudios.UnityHelpers.Core.DataStructure.Adapters; /// /// JSON converter factory for SerializableSortedDictionary types. /// Ensures serialization produces an object with "_keys" and "_values" fields rather than a JSON dictionary, /// which is necessary for proper order preservation across serialization cycles. /// public sealed class SerializableSortedDictionaryConverterFactory : JsonConverterFactory { public static readonly SerializableSortedDictionaryConverterFactory Instance = new(); private SerializableSortedDictionaryConverterFactory() { } public override bool CanConvert(Type typeToConvert) { if (!typeToConvert.IsGenericType) { return false; } Type genericDef = typeToConvert.GetGenericTypeDefinition(); return genericDef == typeof(SerializableSortedDictionary<,>) || genericDef == typeof(SerializableSortedDictionary<,,>); } public override JsonConverter CreateConverter( Type typeToConvert, JsonSerializerOptions options ) { Type genericDef = typeToConvert.GetGenericTypeDefinition(); Type[] typeArgs = typeToConvert.GetGenericArguments(); Type converterType; if (genericDef == typeof(SerializableSortedDictionary<,>)) { // SerializableSortedDictionary where TValueCache = TValue converterType = typeof(SerializableSortedDictionaryConverter<,>).MakeGenericType( typeArgs ); } else { // SerializableSortedDictionary converterType = typeof(SerializableSortedDictionaryWithCacheConverter<,,>).MakeGenericType( typeArgs ); } return (JsonConverter)Activator.CreateInstance(converterType); } private sealed class SerializableSortedDictionaryConverter : JsonConverter> where TKey : IComparable { private const string KeysPropertyName = SerializableDictionarySerializedPropertyNames.Keys; private const string ValuesPropertyName = SerializableDictionarySerializedPropertyNames.Values; public override SerializableSortedDictionary Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options ) { if (reader.TokenType == JsonTokenType.Null) { return null; } if (reader.TokenType != JsonTokenType.StartObject) { throw new JsonException( $"Expected StartObject for SerializableSortedDictionary<{typeof(TKey).Name}, {typeof(TValue).Name}>, got {reader.TokenType}" ); } TKey[] keysArray = null; TValue[] valuesArray = null; while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) { break; } if (reader.TokenType != JsonTokenType.PropertyName) { continue; } string propertyName = reader.GetString(); reader.Read(); if ( string.Equals( propertyName, KeysPropertyName, StringComparison.OrdinalIgnoreCase ) ) { keysArray = JsonSerializer.Deserialize(ref reader, options); } else if ( string.Equals( propertyName, ValuesPropertyName, StringComparison.OrdinalIgnoreCase ) ) { valuesArray = JsonSerializer.Deserialize(ref reader, options); } else { reader.Skip(); } } SerializableSortedDictionary result = new(); if (keysArray != null && valuesArray != null) { SetSerializedArrays(result, keysArray, valuesArray); result.OnAfterDeserialize(); } return result; } public override void Write( Utf8JsonWriter writer, SerializableSortedDictionary value, JsonSerializerOptions options ) { if (value == null) { writer.WriteNullValue(); return; } // Ensure serialized arrays are up to date value.OnBeforeSerialize(); writer.WriteStartObject(); writer.WritePropertyName(KeysPropertyName); JsonSerializer.Serialize(writer, value.SerializedKeys, options); writer.WritePropertyName(ValuesPropertyName); JsonSerializer.Serialize(writer, value.SerializedValues, options); writer.WriteEndObject(); } private static void SetSerializedArrays( SerializableSortedDictionary dict, TKey[] keys, TValue[] values ) { // Use reflection to set the internal _keys and _values fields Type baseType = typeof(SerializableSortedDictionaryBase); FieldInfo keysField = baseType.GetField( KeysPropertyName, BindingFlags.Instance | BindingFlags.NonPublic ); FieldInfo valuesField = baseType.GetField( ValuesPropertyName, BindingFlags.Instance | BindingFlags.NonPublic ); keysField?.SetValue(dict, keys); valuesField?.SetValue(dict, values); } } private sealed class SerializableSortedDictionaryWithCacheConverter< TKey, TValue, TValueCache > : JsonConverter> where TKey : IComparable where TValueCache : SerializableDictionary.Cache, new() { private const string KeysPropertyName = SerializableDictionarySerializedPropertyNames.Keys; private const string ValuesPropertyName = SerializableDictionarySerializedPropertyNames.Values; public override SerializableSortedDictionary Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options ) { if (reader.TokenType == JsonTokenType.Null) { return null; } if (reader.TokenType != JsonTokenType.StartObject) { throw new JsonException( $"Expected StartObject for SerializableSortedDictionary<{typeof(TKey).Name}, {typeof(TValue).Name}, {typeof(TValueCache).Name}>, got {reader.TokenType}" ); } TKey[] keysArray = null; TValueCache[] valuesArray = null; while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) { break; } if (reader.TokenType != JsonTokenType.PropertyName) { continue; } string propertyName = reader.GetString(); reader.Read(); if ( string.Equals( propertyName, KeysPropertyName, StringComparison.OrdinalIgnoreCase ) ) { keysArray = JsonSerializer.Deserialize(ref reader, options); } else if ( string.Equals( propertyName, ValuesPropertyName, StringComparison.OrdinalIgnoreCase ) ) { valuesArray = JsonSerializer.Deserialize( ref reader, options ); } else { reader.Skip(); } } SerializableSortedDictionary result = new(); if (keysArray != null && valuesArray != null) { SetSerializedArrays(result, keysArray, valuesArray); result.OnAfterDeserialize(); } return result; } public override void Write( Utf8JsonWriter writer, SerializableSortedDictionary value, JsonSerializerOptions options ) { if (value == null) { writer.WriteNullValue(); return; } // Ensure serialized arrays are up to date value.OnBeforeSerialize(); writer.WriteStartObject(); writer.WritePropertyName(KeysPropertyName); JsonSerializer.Serialize(writer, value.SerializedKeys, options); writer.WritePropertyName(ValuesPropertyName); JsonSerializer.Serialize(writer, value.SerializedValues, options); writer.WriteEndObject(); } private static void SetSerializedArrays( SerializableSortedDictionary dict, TKey[] keys, TValueCache[] values ) { // Use reflection to set the internal _keys and _values fields Type baseType = typeof(SerializableSortedDictionaryBase); FieldInfo keysField = baseType.GetField( KeysPropertyName, BindingFlags.Instance | BindingFlags.NonPublic ); FieldInfo valuesField = baseType.GetField( ValuesPropertyName, BindingFlags.Instance | BindingFlags.NonPublic ); keysField?.SetValue(dict, keys); valuesField?.SetValue(dict, values); } } } }