using CryptoExchange.Net.Exceptions; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; namespace CryptoExchange.Net.Converters.SystemTextJson { /// /// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties /// with [ArrayProperty(x)] where x is the index of the property in the array /// #if NET5_0_OR_GREATER public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter where T : new() #else public class ArrayConverter : JsonConverter where T : new() #endif { private static SortedDictionary>? _typePropertyInfo; /// #if NET5_0_OR_GREATER [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] #endif public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { if (value == null) { writer.WriteNullValue(); return; } if (_typePropertyInfo == null) _typePropertyInfo = CacheTypeAttributes(); writer.WriteStartArray(); var last = -1; foreach (var indexProps in _typePropertyInfo) { foreach (var prop in indexProps.Value) { if (prop.ArrayProperty.Index == last) // Don't write the same index twice continue; while (prop.ArrayProperty.Index != last + 1) { writer.WriteNullValue(); last += 1; } last = prop.ArrayProperty.Index; var objValue = prop.PropertyInfo.GetValue(value); if (objValue == null) { writer.WriteNullValue(); continue; } JsonSerializerOptions? typeOptions = null; if (prop.JsonConverter != null) { typeOptions = new JsonSerializerOptions { NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, PropertyNameCaseInsensitive = false, TypeInfoResolver = options.TypeInfoResolver, }; typeOptions.Converters.Add(prop.JsonConverter); } if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType)) { if (prop.TargetType == typeof(string)) writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); else if (prop.TargetType == typeof(bool)) writer.WriteBooleanValue((bool)objValue); else writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!); } else { JsonSerializer.Serialize(writer, objValue, typeOptions ?? options); } } } writer.WriteEndArray(); } /// public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) return default; var result = new T(); return ParseObject(ref reader, result, options); } #if NET5_0_OR_GREATER [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options) #else private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options) #endif { if (reader.TokenType != JsonTokenType.StartArray) throw new CeDeserializationException("Not an array"); if (_typePropertyInfo == null) _typePropertyInfo = CacheTypeAttributes(); int index = 0; while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndArray) break; if(!_typePropertyInfo.TryGetValue(index, out var indexAttributes)) { index++; continue; } foreach (var attribute in indexAttributes) { var targetType = attribute.TargetType; object? value = null; if (attribute.JsonConverter != null) { if (attribute.JsonSerializerOptions == null) { attribute.JsonSerializerOptions = new JsonSerializerOptions { NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, PropertyNameCaseInsensitive = false, Converters = { attribute.JsonConverter }, TypeInfoResolver = options.TypeInfoResolver, }; } var doc = JsonDocument.ParseValue(ref reader); value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions); } else if (attribute.DefaultDeserialization) { value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType)); } else { value = reader.TokenType switch { JsonTokenType.Null => null, JsonTokenType.False => false, JsonTokenType.True => true, JsonTokenType.String => reader.GetString(), JsonTokenType.Number => reader.GetDecimal(), JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options), _ => throw new CeDeserializationException($"Array deserialization of type {reader.TokenType} not supported"), }; } if (targetType.IsAssignableFrom(value?.GetType())) attribute.PropertyInfo.SetValue(result, value); else attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture)); } index++; } return result; } private static bool IsSimple(Type type) { if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) { // nullable type, check if the nested type is simple. return IsSimple(type.GetGenericArguments()[0]); } return type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal); } #if NET5_0_OR_GREATER [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] private static SortedDictionary> CacheTypeAttributes() #else private static SortedDictionary> CacheTypeAttributes() #endif { var result = new SortedDictionary>(); var properties = typeof(T).GetProperties(); foreach (var property in properties) { var att = property.GetCustomAttribute(); if (att == null) continue; var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; var converterType = property.GetCustomAttribute()?.ConverterType ?? targetType.GetCustomAttribute()?.ConverterType; if (!result.TryGetValue(att.Index, out var indexList)) { indexList = new List(); result[att.Index] = indexList; } indexList.Add(new ArrayPropertyInfo { ArrayProperty = att, PropertyInfo = property, DefaultDeserialization = property.GetCustomAttribute() != null, JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!, TargetType = targetType }); } return result; } private class ArrayPropertyInfo { public PropertyInfo PropertyInfo { get; set; } = null!; public ArrayPropertyAttribute ArrayProperty { get; set; } = null!; public JsonConverter? JsonConverter { get; set; } public bool DefaultDeserialization { get; set; } public Type TargetType { get; set; } = null!; public JsonSerializerOptions? JsonSerializerOptions { get; set; } = null; } } }