Далее речь пойдет о подготовке классов для более тонкой настройки преобразования данных, но в начале необходимо вернуться к самому парсингу JSON. Напомню в первой части были использованы методы Parse() и ToObject<Т>() классов JObject и JArray пространства имен newtonsoft.json.linq:
HttpClient httpClient = new HttpClient(); string request = "https://api.exmo.com/v1/trades/?pair=BTC_USD,ETH_USD"; HttpResponseMessage response = (await httpClient.GetAsync(request)).EnsureSuccessStatusCode(); string responseBody = await response.Content.ReadAsStringAsync(); JObject jObject = JObject.Parse(responseBody); Dictionary<string, List<Order>> dict = jObject.ToObject<Dictionary<string, List<Order>>>();
Необходимо отметить, что в пространстве имен newtonsof.json в классе JsonConvert есть статический метод DeserializeObject<>, позволяющий преобразовывать строки напрямую в структуры C#, соответствующие объектам и массивам нотации JSON:
Dictionary<string, List<Order>> JsonObject = JsonConvert.DeserializeObject<Dictionary<string, List<Order>>>(responseBody); List<string> Json_Array = JsonConvert.DeserializeObject<List<string>>(responseBody);
И в дальнейшем в статье будет использован именно этот метод, поэтому в программу нужно добавить using newtonsoft.json.
Кроме того, есть возможность еще больше сократить количество промежуточных преобразований — после установки библиотеки Microsoft.AspNet.WebApi.Client (так же доступна через NuGet), данные можно будет парсить прямо из потока используя метод ReadAsAsync:
Dictionary<string, List<Order>> JsonObject = await (await httpClient.GetAsync(request)). EnsureSuccessStatusCode().Content. ReadAsAsync<Dictionary<string, List<Order>>>();
За подсказку спасибо lair.
Подготовка класса для преобразования
Вернемся к нашему классу Order:
class Order { public int trade_id { get; set; } public string type { get; set; } public double quantity { get; set; } public double price { get; set; } public double amount { get; set; } public int date { get; set; } }
Напомню, он был создан на основе формата, предложенного JSON C# Class Generator`ом. Есть два момента, которые при работе с объектами данного класса могут вызвать сложности.
Первый — в таком виде свойства нашего класса нарушают правила наименования полей. Ну и кроме того, логично для объекта типа Order ожидать что его идентификатор будет называться OrderID (а не traid_id, как происходило в примерах из первой части). Чтобы связать элемент структуры JSON и свойство класса с произвольным именем, необходимо перед свойством добавить атрибут JsonProperty:
class Order { [JsonProperty("trade_id")] public int OrderID { get; set; } [JsonProperty("type")] public string Type { get; set; } [JsonProperty("quantity")] public double Quantity { get; set; } [JsonProperty("price")] public double Price { get; set; } [JsonProperty("amount")] public double Amount { get; set; } [JsonProperty("date")] public int Date { get; set; } }
В результате, значение, соответствующее элементу “trade_id”, будет записано в свойство OrderID, “type” в Type и т.д. Так же атрибут JsonProperty необходим для сериализации / десериализации свойств, имеющих модификаторы private или static — по умолчанию такие свойства игнорируются.
В итоге, код для составления списка всех OrderID сделок по валютным парам BTC_USD и ETH_USD, может выглядеть следующим образом:
using HttpClient httpClient = new HttpClient(); string request = "https://api.exmo.com/v1/trades/?pair=BTC_USD,ETH_USD"; string responseBody = await (await httpClient.GetAsync(request)). EnsureSuccessStatusCode(). Content.ReadAsStringAsync(); Dictionary<string, List<Order>> PairList = JsonConvert.DeserializeObject<Dictionary<string, List<Order>>>(responseBody); List<int> IDs = new List<int>(); foreach (var pair in PairList) foreach (var order in pair.Value) IDs.Add(order.OrderID);
Вторая сложность при работе с данным классом будет заключаться в свойстве Date. Как можно увидеть, JSON C# Class Generator определил элемент “date” как простое целочисленное число. Но гораздо удобнее было бы, чтобы свойство Date нашего класса имело тип специально созданный для дат — DateTime. Как это сделать — будет описано далее.
Особенности работы с датами
Начальную фразу статьи в документации по newtonsof.json, с описанием работы с датами, можно примерно перевести как “DateTime в JSON — это тяжко”. Проблема заключается в том, что сама спецификация JSON не содержит информации о том, какой синтаксис необходимо применять для описания даты и времени.
Все относительно неплохо, когда дата в JSON строке представлена в текстовом виде и формат представления соответствует одному из трех вариантов: “Майкрософт” (в настоящее время считается устаревшим), “JavaScript” (Unix время) и вариант ISO 8601. Примеры допустимых форматов:
Dictionary<string, DateTime> d = new Dictionary<string, DateTime> { { "date", DateTime.Now } }; string isoDate = JsonConvert.SerializeObject(d); // {"date":"2019-12-19T14:10:31.3708939+03:00"} JsonSerializerSettings microsoftDateFormatSettings = new JsonSerializerSettings { DateFormatHandling = DateFormatHandling.MicrosoftDateFormat }; string microsoftDate = JsonConvert.SerializeObject(d, microsoftDateFormatSettings); // {"date":"\/Date(1576753831370+0300)\/"} string javascriptDate = JsonConvert.SerializeObject(d, new JavaScriptDateTimeConverter()); // {"date":new Date(1576753831370)}
Однако в случае с биржей Exmo, все немного сложнее. В описании API биржи указано, что дата и время указываются в формате Unix (JavaScript). И, теоретически, добавив к нашему свойству Date класса Order функцию преобразования формата из класса JavaScriptDateTimeConverter(), мы должны получить дату, приведенную к типу DateTime:
class Order { [JsonProperty("trade_id")] public int OrderID { get; set; } [JsonProperty("type")] public string Type { get; set; } [JsonProperty("quantity")] public double Quantity { get; set; } [JsonProperty("price")] public double Price { get; set; } [JsonProperty("amount")] public double Amount { get; set; } [JsonProperty("date", ItemConverterType = typeof(JavaScriptDateTimeConverter))] public DateTime Date { get; set; } }
Однако в этом случае, при попытке парсинга данных в переменную типа DateTime появляется уже знакомое по первой части статьи исключение Newtonsoft.Json.JsonReaderException. Происходит это по причине того, что функция преобразования класса JavaScriptDateTimeConverter не умеет конвертировать числовые данные в тип DateTime (это работает только для строк).
Возможный выход из данной ситуации — написать свой собственный класс преобразования форматов. На самом деле такой класс уже есть и его можно использовать, предварительно подключив пространство имен Newtonsoft.Json.Converters (обратите внимание, что обратная функция — конвертирования из DateTime в формат JSON в данном классе не реализована):
class UnixTimeToDatetimeConverter : DateTimeConverterBase { private static readonly DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.Value == null) { return null; } return _epoch.AddSeconds(Convert.ToDouble(reader.Value)).ToLocalTime(); } }
Остается только подключить нашу функцию к свойству Date класса Order. Для этого необходимо использовать атрибут JsonConverter:
class Order { [JsonProperty("trade_id")] public int OrderID { get; set; } [JsonProperty("type")] public string Type { get; set; } [JsonProperty("quantity")] public double Quantity { get; set; } [JsonProperty("price")] public double Price { get; set; } [JsonProperty("amount")] public double Amount { get; set; } [JsonProperty("date")] [JsonConverter(typeof(UnixTimeToDatetimeConverter))] public DateTime Date { get; set; } }
Теперь наше свойство Date имеет тип DateTime и мы можем, например, сформировать список сделок, за последние 10 минут:
HttpClient httpClient = new HttpClient(); string request = "https://api.exmo.com/v1/trades/?pair=BTC_USD,ETH_USD"; string responseBody = await (await httpClient.GetAsync(request)). EnsureSuccessStatusCode(). Content.ReadAsStringAsync(); Dictionary<string, List<Order>> PairList = JsonConvert.DeserializeObject<Dictionary<string, List<Order>>>(responseBody); List<Order> Last10minuts = new List<Order>(); foreach (var pair in PairList) foreach (var order in pair.Value) if (order.Date > DateTime.Now.AddMinutes(-10)) Last10minuts.Add(order);
using System; using System.Threading.Tasks; using System.Collections.Generic; using System.Net.Http; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; namespace JSONObjects { class Order { [JsonProperty("pair")] public string Pair { get; set; } [JsonProperty("trade_id")] public int OrderID { get; set; } [JsonProperty("type")] public string Type { get; set; } [JsonProperty("quantity")] public double Quantity { get; set; } [JsonProperty("price")] public double Price { get; set; } [JsonProperty("amount")] public double Amount { get; set; } [JsonProperty("date")] [JsonConverter(typeof(UnixTimeToDatetimeConverter))] public DateTime Date { get; set; } } class UnixTimeToDatetimeConverter : DateTimeConverterBase { private static readonly DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.Value == null) { return null; } return _epoch.AddSeconds(Convert.ToDouble(reader.Value)).ToLocalTime(); } } class Program { public static async Task Main(string[] args) { using HttpClient httpClient = new HttpClient(); string request = "https://api.exmo.com/v1/trades/?pair=BTC_USD,ETH_USD"; string responseBody = await (await httpClient.GetAsync(request)). EnsureSuccessStatusCode(). Content.ReadAsStringAsync(); Dictionary<string, List<Order>> PairList = JsonConvert. DeserializeObject<Dictionary<string, List<Order>>>(responseBody); List<Order> Last10minuts = new List<Order>(); foreach (var pair in PairList) foreach (var order in pair.Value) if (order.Date > DateTime.Now.AddMinutes(-10)) Last10minuts.Add(order); } } }
Подмена имен элементов JSON
Ранее мы работали с командой trades биржи. Данная команда возвращает объекты со следующими полями:
public class BTCUSD { public int trade_id { get; set; } public string type { get; set; } public string quantity { get; set; } public string price { get; set; } public string amount { get; set; } public int date { get; set; } }
Команда биржи user_open_orders возвращает очень похожую структуру:
public class BTCUSD { public string order_id { get; set; } public string created { get; set; } public string type { get; set; } public string pair { get; set; } public string quantity { get; set; } public string price { get; set; } public string amount { get; set; } }
Поэтому имеет смысл адаптировать класс Order, чтобы в него можно было преобразовывать не только данные команды trade, но также и данные команды user_open_orders.
Отличия заключаются в том, что появился новый элемент pair, содержащий название валютной пары, trade_id заменен на order_id (и теперь это строка), а date стала created и тоже является строкой.
Начнем с того, что добавим возможность сохранения полей order_id и created в соответствующие поля OrderID и Date класса Order. Для этого подготовим класс OrderDataContractResolver, в котором будет происходить подмена имен полей для парсинга (потребуются пространства имен System.Reflection и Newtonsoft.Json.Serialization):
class OrderDataContractResolver : DefaultContractResolver { public static readonly OrderDataContractResolver Instance = new OrderDataContractResolver(); protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); if (property.DeclaringType == typeof(Order)) { if (property.PropertyName.Equals("trade_id", StringComparison.OrdinalIgnoreCase)) { property.PropertyName = "order_id"; } if (property.PropertyName.Equals("date", StringComparison.OrdinalIgnoreCase)) { property.PropertyName = "created"; } } return property; } }
Далее этот класс необходимо указать в качестве параметра метода DeserializeObject следующим образом:
Dictionary<string, List<Order>> PairList = JsonConvert.DeserializeObject<Dictionary<string, List<Order>>>(responseBody, new JsonSerializerSettings { ContractResolver = OrderDataContractResolver.Instance });
В результате, такая JSON структура, полученная в качестве ответа на команду user_open_orders:
{"BTC_USD":[{"order_id":"4722868563","created":"1577349229","type":"sell","pair":"BTC_USD","quantity":"0.002","price":"8362.2","amount":"16.7244"}]}
будет преобразована в такую структуру данных:
Обратите внимание, что для корректной работы программы, тип поля OrderID в классе Order пришлось заменить на string.
using System; using System.Threading.Tasks; using System.Collections.Generic; using System.Net.Http; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; using System.Reflection; namespace JSONObjects { class Order { [JsonProperty("pair")] public string Pair { get; set; } [JsonProperty("trade_id")] public string OrderID { get; set; } [JsonProperty("type")] public string Type { get; set; } [JsonProperty("quantity")] public double Quantity { get; set; } [JsonProperty("price")] public double Price { get; set; } [JsonProperty("amount")] public double Amount { get; set; } [JsonProperty("date")] [JsonConverter(typeof(UnixTimeToDatetimeConverter))] public DateTime Date { get; set; } } class OrderDataContractResolver : DefaultContractResolver { public static readonly OrderDataContractResolver Instance = new OrderDataContractResolver(); protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); if (property.DeclaringType == typeof(Order)) { if (property.PropertyName.Equals("trade_id", StringComparison.OrdinalIgnoreCase)) { property.PropertyName = "order_id"; } if (property.PropertyName.Equals("date", StringComparison.OrdinalIgnoreCase)) { property.PropertyName = "created"; } } return property; } } class UnixTimeToDatetimeConverter : DateTimeConverterBase { private static readonly DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.Value == null) { return null; } return _epoch.AddSeconds(Convert.ToDouble(reader.Value)).ToLocalTime(); } } class Program { public static async Task Main(string[] args) { using HttpClient httpClient = new HttpClient(); string request = "https://api.exmo.com/v1/trades/?pair=BTC_USD,ETH_USD"; string responseBody = await (await httpClient.GetAsync(request)). EnsureSuccessStatusCode(). Content.ReadAsStringAsync(); Dictionary<string, List<Order>> PairList = new Dictionary<string, List<Order>>(); JObject jObject = JObject.Parse(responseBody); foreach (var pair in jObject) { List<Order> orders = new List<Order>(); foreach (var order in pair.Value.ToObject<List<Order>>()) { order.Pair = pair.Key; orders.Add(order); } PairList.Add(pair.Key, orders); } responseBody = "{\"BTC_USD\":[{\"order_id\":\"4722868563\"," + "\"created\":\"1577349229\",\"type\":\"sell\"," + "\"pair\":\"BTC_USD\",\"quantity\":\"0.002\"," + "\"price\":\"8362.2\",\"amount\":\"16.7244\"}]}"; Dictionary<string, List<Order>> PairList1 = JsonConvert.DeserializeObject<Dictionary<string, List<Order>>> (responseBody, new JsonSerializerSettings { ContractResolver = OrderDataContractResolver.Instance }); } } } }
Как можно заметить, при вызове команды user_open_orders, ответ содежит поле “pair”, в случае же команды trade информация о валютной паре содержится только в значении ключа. Поэтому придется либо заполнить поле Pair уже после парсинга:
foreach (var pair in PairList) foreach (var order in pair.Value) order.Pair = pair.Key;
Либо же воспользоваться объектом JObject:
Dictionary<string, List<Order>> PairList = new Dictionary<string, List<Order>>(); JObject jObject = JObject.Parse(responseBody); foreach (var pair in jObject) { List<Order> orders = new List<Order>(); foreach (var order in pair.Value.ToObject<List<Order>>()) { order.Pair = pair.Key; orders.Add(order); } PairList.Add(pair.Key, orders); }
Что в конечном итоге приведет к созданию следующей структуры данных:
ссылка на оригинал статьи https://habr.com/ru/post/482042/
Добавить комментарий