В первой части статьи был рассмотрен механизм парсинга объектов JSON с динамически изменяющейся структурой. Данные объекты приводились к типам пространства имен newtonsoft.json.linq, и затем преобразовывались в структуры языка C#. В комментариях к первой части было много критики, по большей части обоснованной. Во второй части я постараюсь учесть все замечания и пожелания.
Далее речь пойдет о подготовке классов для более тонкой настройки преобразования данных, но в начале необходимо вернуться к самому парсингу JSON. Напомню в первой части были использованы методы Parse() и ToObject<Т>() классов JObject и JArray пространства имен newtonsoft.json.linq:
Необходимо отметить, что в пространстве имен newtonsof.json в классе JsonConvert есть статический метод DeserializeObject<>, позволяющий преобразовывать строки напрямую в структуры C#, соответствующие объектам и массивам нотации JSON:
И в дальнейшем в статье будет использован именно этот метод, поэтому в программу нужно добавить using newtonsoft.json.
Кроме того, есть возможность еще больше сократить количество промежуточных преобразований — после установки библиотеки Microsoft.AspNet.WebApi.Client (так же доступна через NuGet), данные можно будет парсить прямо из потока используя метод ReadAsAsync:
За подсказку спасибо lair.
Вернемся к нашему классу Order:
Напомню, он был создан на основе формата, предложенного JSON C# Class Generator`ом. Есть два момента, которые при работе с объектами данного класса могут вызвать сложности.
Первый — в таком виде свойства нашего класса нарушают правила наименования полей. Ну и кроме того, логично для объекта типа Order ожидать что его идентификатор будет называться OrderID (а не traid_id, как происходило в примерах из первой части). Чтобы связать элемент структуры JSON и свойство класса с произвольным именем, необходимо перед свойством добавить атрибут JsonProperty:
В результате, значение, соответствующее элементу “trade_id”, будет записано в свойство OrderID, “type” в Type и т.д. Так же атрибут JsonProperty необходим для сериализации / десериализации свойств, имеющих модификаторы private или static — по умолчанию такие свойства игнорируются.
В итоге, код для составления списка всех OrderID сделок по валютным парам BTC_USD и ETH_USD, может выглядеть следующим образом:
Вторая сложность при работе с данным классом будет заключаться в свойстве Date. Как можно увидеть, JSON C# Class Generator определил элемент “date” как простое целочисленное число. Но гораздо удобнее было бы, чтобы свойство Date нашего класса имело тип специально созданный для дат — DateTime. Как это сделать — будет описано далее.
Начальную фразу статьи в документации по newtonsof.json, с описанием работы с датами, можно примерно перевести как “DateTime в JSON — это тяжко”. Проблема заключается в том, что сама спецификация JSON не содержит информации о том, какой синтаксис необходимо применять для описания даты и времени.
Все относительно неплохо, когда дата в JSON строке представлена в текстовом виде и формат представления соответствует одному из трех вариантов: “Майкрософт” (в настоящее время считается устаревшим), “JavaScript” (Unix время) и вариант ISO 8601. Примеры допустимых форматов:
Однако в случае с биржей Exmo, все немного сложнее. В описании API биржи указано, что дата и время указываются в формате Unix (JavaScript). И, теоретически, добавив к нашему свойству Date класса Order функцию преобразования формата из класса JavaScriptDateTimeConverter(), мы должны получить дату, приведенную к типу DateTime:
Однако в этом случае, при попытке парсинга данных в переменную типа DateTime появляется уже знакомое по первой части статьи исключение Newtonsoft.Json.JsonReaderException. Происходит это по причине того, что функция преобразования класса JavaScriptDateTimeConverter не умеет конвертировать числовые данные в тип DateTime (это работает только для строк).
Возможный выход из данной ситуации — написать свой собственный класс преобразования форматов. На самом деле такой класс уже есть и его можно использовать, предварительно подключив пространство имен Newtonsoft.Json.Converters (обратите внимание, что обратная функция — конвертирования из DateTime в формат JSON в данном классе не реализована):
Остается только подключить нашу функцию к свойству Date класса Order. Для этого необходимо использовать атрибут JsonConverter:
Теперь наше свойство Date имеет тип DateTime и мы можем, например, сформировать список сделок, за последние 10 минут:
Ранее мы работали с командой trades биржи. Данная команда возвращает объекты со следующими полями:
Команда биржи user_open_orders возвращает очень похожую структуру:
Поэтому имеет смысл адаптировать класс Order, чтобы в него можно было преобразовывать не только данные команды trade, но также и данные команды user_open_orders.
Отличия заключаются в том, что появился новый элемент pair, содержащий название валютной пары, trade_id заменен на order_id (и теперь это строка), а date стала created и тоже является строкой.
Начнем с того, что добавим возможность сохранения полей order_id и created в соответствующие поля OrderID и Date класса Order. Для этого подготовим класс OrderDataContractResolver, в котором будет происходить подмена имен полей для парсинга (потребуются пространства имен System.Reflection и Newtonsoft.Json.Serialization):
Далее этот класс необходимо указать в качестве параметра метода DeserializeObject следующим образом:
В результате, такая JSON структура, полученная в качестве ответа на команду user_open_orders:
будет преобразована в такую структуру данных:
Обратите внимание, что для корректной работы программы, тип поля OrderID в классе Order пришлось заменить на string.
Как можно заметить, при вызове команды user_open_orders, ответ содежит поле “pair”, в случае же команды trade информация о валютной паре содержится только в значении ключа. Поэтому придется либо заполнить поле Pair уже после парсинга:
Либо же воспользоваться объектом JObject:
Что в конечном итоге приведет к созданию следующей структуры данных:
Далее речь пойдет о подготовке классов для более тонкой настройки преобразования данных, но в начале необходимо вернуться к самому парсингу 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);
}
Что в конечном итоге приведет к созданию следующей структуры данных: