Code/Json.cs
using System;
using System.Collections;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Sandbox;
using Sandbox.Diagnostics;

namespace DataTables;

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class JsonTypeAnnotateAttribute : Attribute
{
}

internal static class Json
{
	public static JsonSerializerOptions Options()
	{
		return new JsonSerializerOptions() { WriteIndented = true };
	}

	public static JsonNode Serialize( object target, bool typeAnnotate, Type typeOverride = null )
	{
		var type = target.GetType();
		var typeDesc = TypeLibrary.GetType( type );

		if ( typeDesc.IsValueType || type.IsAssignableTo( typeof(Resource) ) ||
		     type.IsAssignableTo( typeof(string) ) )
			return Sandbox.Json.ToNode( target );

		if ( type.IsAssignableTo( typeof(IList) ) )
			return SerializeList( (IList)target, typeAnnotate );

		if ( type.IsAssignableTo( typeof(IDictionary) ) )
			return SerializeDictionary( (IDictionary)target, typeAnnotate );

		var node = SerializeObject( target, true, typeOverride );
		if ( typeAnnotate )
			node["__type"] = typeDesc.FullName;
		return node;
	}

	public static JsonNode SerializeDictionary( IDictionary target, bool typeAnnotate )
	{
		JsonObject jdict = new();

		Type keyArg = TypeLibrary.GetGenericArguments( target.GetType() )[0];

		bool isInteger = keyArg == typeof(int);
		bool isString = keyArg == typeof(string);
		bool isReal = keyArg == typeof(float) || keyArg == typeof(double);

		if ( !(isInteger || isString || isReal) )
		{
			Log.Error(
				$"The type '{keyArg.FullName}' is not a supported dictionary key! If you really need this to be supported, please submit an issue @ https://github.com/tzainten/DataTables" );
			return jdict;
		}

		foreach ( var key in target.Keys )
		{
			if ( key is null )
				continue;

			var value = target[key];
			if ( value is null )
				continue;

			jdict.Add( key.ToString(), Serialize( value, typeAnnotate ) );
		}

		return jdict;
	}

	public static JsonArray SerializeList( IList target, bool typeAnnotate )
	{
		JsonArray jarray = new();

		foreach ( var elem in target )
		{
			if ( elem is null )
				continue;

			jarray.Add( Serialize( elem, typeAnnotate ) );
		}

		return jarray;
	}

	public static JsonObject SerializeObject( object target, bool typeAnnotate, Type typeOverride = null )
	{
		JsonObject jobj = new();

		var type = typeOverride ?? target.GetType();
		var typeDesc = TypeLibrary.GetType( type );

		if ( typeDesc.IsValueType || type.IsAssignableTo( typeof(Resource) ) ||
		     type.IsAssignableTo( typeof(string) ) )
			return Sandbox.Json.ToNode( target ).AsObject();

		var members = TypeLibrary.GetFieldsAndProperties( typeDesc );
		foreach ( var member in members )
		{
			object value = null;
			bool shouldAnnotate = false;

			if ( member.IsField )
			{
				FieldDescription field = (FieldDescription)member;
				value = field.GetValue( target );
				if ( value is null )
					continue;

				shouldAnnotate = field.HasAttribute( typeof(JsonTypeAnnotateAttribute) );
				jobj[field.Name] = Serialize( value, shouldAnnotate, !shouldAnnotate ? field.FieldType : null );

				continue;
			}

			PropertyDescription property = (PropertyDescription)member;
			value = property.GetValue( target );
			if ( value is null )
				continue;

			shouldAnnotate = property.HasAttribute( typeof(JsonTypeAnnotateAttribute) );
			jobj[property.Name] = Serialize( value, shouldAnnotate, !shouldAnnotate ? property.PropertyType : null );
		}

		return jobj;
	}

	public static T Deserialize<T>( string json )
	{
		JsonNode node = JsonNode.Parse( json );
		if ( node is null )
			return default;

		return (T)DeserializeInternal( node, typeof(T) );
	}

	public static object DeserializeInternal( JsonNode node, Type type )
	{
		TypeDescription typeDesc = TypeLibrary.GetType( type );
		if ( typeDesc is not null && (typeDesc.IsValueType || type.IsAssignableTo( typeof(Resource) ) ||
		                              type.IsAssignableTo( typeof(string) )) )
		{
			try
			{
				return Sandbox.Json.FromNode( node, type );
			}
			catch ( Exception e )
			{
				return null;
			}
		}

		if ( type.IsAssignableTo( typeof(IDictionary) ) )
			return DeserializeDictionary( node.AsObject(), type );

		switch ( node.GetValueKind() )
		{
			case JsonValueKind.Object:
				return DeserializeObject( node.AsObject(), type );
			case JsonValueKind.Array:
				return DeserializeList( node.AsArray(), type );
			default:
				try
				{
					return Sandbox.Json.FromNode( node, type );
				}
				catch ( Exception e )
				{
					return null;
				}
		}
	}

	public static IList DeserializeList( JsonArray jarray, Type type )
	{
		IList list = TypeLibrary.Create<IList>( type );

		using var enumerator = jarray.GetEnumerator();
		while ( enumerator.MoveNext() )
		{
			var node = enumerator.Current;

			Type genericArg = TypeLibrary.GetGenericArguments( type ).First();

			var elem = DeserializeInternal( node, genericArg );
			if ( elem is null )
				continue;

			if ( elem.GetType().IsAssignableTo( genericArg ) )
				list.Add( elem );
		}

		return list;
	}

	public static IDictionary DeserializeDictionary( JsonObject jobj, Type type )
	{
		IDictionary dict = TypeLibrary.Create<IDictionary>( type );

		using var enumerator = jobj.GetEnumerator();
		while ( enumerator.MoveNext() )
		{
			var pair = enumerator.Current;

			Type[] genericArgs = TypeLibrary.GetGenericArguments( type );
			var keyType = genericArgs[0];

			var key = pair.Key;
			object parsedKey = key;
			if ( keyType == typeof(int) )
			{
				if ( int.TryParse( key, out int num ) )
					parsedKey = num;
			}
			else if ( keyType == typeof(double) )
			{
				if ( double.TryParse( key, out double num ) )
					parsedKey = num;
			}
			else if ( keyType == typeof(float) )
			{
				if ( float.TryParse( key, out float num ) )
					parsedKey = num;
			}

			var node = pair.Value;

			var elem = DeserializeInternal( node, genericArgs[1] );
			if ( elem is null )
				continue;

			Type keyArg = TypeLibrary.GetGenericArguments( type )[0];
			Type valueArg = TypeLibrary.GetGenericArguments( type )[1];

			bool isCorrectKeyType = parsedKey.GetType().IsAssignableTo( keyArg );
			bool isCorrectValueType = elem.GetType().IsAssignableTo( valueArg );

			if ( !isCorrectKeyType || !isCorrectValueType )
				continue;

			if ( elem.GetType().IsAssignableTo( genericArgs[1] ) )
				dict.Add( parsedKey, elem );
		}

		return dict;
	}

	public static object DeserializeObject( JsonObject jobj, Type type )
	{
		jobj.TryGetPropertyValue( "__type", out JsonNode __type );

		TypeDescription typeDesc = null;
		if ( __type is not null )
		{
			typeDesc = TypeLibrary.GetType( __type.GetValue<string>() );
		}
		else
		{
			typeDesc = TypeLibrary.GetType( type );
		}

		if ( typeDesc is null )
			return null;

		object instance = TypeLibrary.Create<object>( typeDesc.TargetType );
		using var enumerator = jobj.GetEnumerator();
		while ( enumerator.MoveNext() )
		{
			var node = enumerator.Current;

			var property = typeDesc.Properties.FirstOrDefault( x =>
				x.IsPublic && !x.IsStatic && x.IsNamed( node.Key ) && x.CanWrite && x.CanRead );
			bool isValidProperty = property is not null;

			var field = typeDesc.Fields.FirstOrDefault( x => x.IsPublic && !x.IsStatic && x.IsNamed( node.Key ) );
			bool isValidField = field is not null;

			if ( !isValidProperty && !isValidField )
				continue;

			var deserializeType = isValidProperty ? property.PropertyType : field.FieldType;
			var value = DeserializeInternal( node.Value, deserializeType );
			if ( value is null )
				continue;

			if ( value.GetType().IsAssignableTo( deserializeType ) )
			{
				if ( isValidProperty )
					property.SetValue( instance, value );
				else
					field.SetValue( instance, value );
			}
		}

		return instance;
	}
}