// 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.Collections.Generic;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using WallstopStudios.UnityHelpers.Core.DataStructure.Adapters;
///
/// JSON converter factory for SerializableHashSet and SerializableSortedSet types.
/// Ensures serialization produces an object with "_items" field rather than a JSON array,
/// which is necessary for proper order preservation across serialization cycles.
///
public sealed class SerializableSetConverterFactory : JsonConverterFactory
{
public static readonly SerializableSetConverterFactory Instance = new();
private SerializableSetConverterFactory() { }
public override bool CanConvert(Type typeToConvert)
{
if (!typeToConvert.IsGenericType)
{
return false;
}
Type genericDef = typeToConvert.GetGenericTypeDefinition();
return genericDef == typeof(SerializableHashSet<>)
|| genericDef == typeof(SerializableSortedSet<>);
}
public override JsonConverter CreateConverter(
Type typeToConvert,
JsonSerializerOptions options
)
{
Type elementType = typeToConvert.GetGenericArguments()[0];
Type genericDef = typeToConvert.GetGenericTypeDefinition();
Type converterType;
if (genericDef == typeof(SerializableHashSet<>))
{
converterType = typeof(SerializableHashSetConverter<>).MakeGenericType(elementType);
}
else
{
converterType = typeof(SerializableSortedSetConverter<>).MakeGenericType(
elementType
);
}
return (JsonConverter)Activator.CreateInstance(converterType);
}
private sealed class SerializableHashSetConverter : JsonConverter>
{
private const string ItemsPropertyName =
SerializableHashSetSerializedPropertyNames.Items;
public override SerializableHashSet Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options
)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}
// Handle array format (legacy/fallback)
if (reader.TokenType == JsonTokenType.StartArray)
{
T[] items = JsonSerializer.Deserialize(ref reader, options);
SerializableHashSet set = new();
if (items != null)
{
SetItemsField(set, items);
set.OnAfterDeserialize();
}
return set;
}
// Handle object format with _items property
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException(
$"Expected StartObject or StartArray for SerializableHashSet<{typeof(T).Name}>, got {reader.TokenType}"
);
}
T[] itemsArray = 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,
ItemsPropertyName,
StringComparison.OrdinalIgnoreCase
)
)
{
itemsArray = JsonSerializer.Deserialize(ref reader, options);
}
else
{
reader.Skip();
}
}
SerializableHashSet result = new();
if (itemsArray != null)
{
SetItemsField(result, itemsArray);
result.OnAfterDeserialize();
}
return result;
}
public override void Write(
Utf8JsonWriter writer,
SerializableHashSet value,
JsonSerializerOptions options
)
{
if (value == null)
{
writer.WriteNullValue();
return;
}
// Ensure serialized arrays are up to date
value.OnBeforeSerialize();
writer.WriteStartObject();
writer.WritePropertyName(ItemsPropertyName);
JsonSerializer.Serialize(writer, value.SerializedItems, options);
writer.WriteEndObject();
}
private static void SetItemsField(SerializableHashSet set, T[] items)
{
// Use reflection to set the internal _items field
Type type = typeof(SerializableSetBase>);
FieldInfo field = type.GetField(
ItemsPropertyName,
BindingFlags.Instance | BindingFlags.NonPublic
);
field?.SetValue(set, items);
}
}
private sealed class SerializableSortedSetConverter
: JsonConverter>
where T : IComparable
{
private const string ItemsPropertyName =
SerializableHashSetSerializedPropertyNames.Items;
public override SerializableSortedSet Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options
)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}
// Handle array format (legacy/fallback)
if (reader.TokenType == JsonTokenType.StartArray)
{
T[] items = JsonSerializer.Deserialize(ref reader, options);
SerializableSortedSet set = new();
if (items != null)
{
SetItemsField(set, items);
set.OnAfterDeserialize();
}
return set;
}
// Handle object format with _items property
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException(
$"Expected StartObject or StartArray for SerializableSortedSet<{typeof(T).Name}>, got {reader.TokenType}"
);
}
T[] itemsArray = 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,
ItemsPropertyName,
StringComparison.OrdinalIgnoreCase
)
)
{
itemsArray = JsonSerializer.Deserialize(ref reader, options);
}
else
{
reader.Skip();
}
}
SerializableSortedSet result = new();
if (itemsArray != null)
{
SetItemsField(result, itemsArray);
result.OnAfterDeserialize();
}
return result;
}
public override void Write(
Utf8JsonWriter writer,
SerializableSortedSet value,
JsonSerializerOptions options
)
{
if (value == null)
{
writer.WriteNullValue();
return;
}
// Ensure serialized arrays are up to date
value.OnBeforeSerialize();
writer.WriteStartObject();
writer.WritePropertyName(ItemsPropertyName);
JsonSerializer.Serialize(writer, value.SerializedItems, options);
writer.WriteEndObject();
}
private static void SetItemsField(SerializableSortedSet set, T[] items)
{
// Use reflection to set the internal _items field
Type type = typeof(SerializableSetBase>);
FieldInfo field = type.GetField(
ItemsPropertyName,
BindingFlags.Instance | BindingFlags.NonPublic
);
field?.SetValue(set, items);
}
}
}
}