Работа с библиотекой Newtonsoft.Json на реальном примере. Часть 2

от автора

В первой части статьи был рассмотрен механизм парсинга объектов JSON с динамически изменяющейся структурой. Данные объекты приводились к типам пространства имен newtonsoft.json.linq, и затем преобразовывались в структуры языка C#. В комментариях к первой части было много критики, по большей части обоснованной. Во второй части я постараюсь учесть все замечания и пожелания.


Далее речь пойдет о подготовке классов для более тонкой настройки преобразования данных, но в начале необходимо вернуться к самому парсингу 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *