// 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);
}
}
}
}