System.Text.Json - 无法反序列化 REST 响应
System.Text.Json - failing to deserialize a REST response
我正在尝试实现以下 API Endpoint。由于 System.Text.Json
现在比 Newtonsoft.Json 更受欢迎,我决定尝试一下。响应显然有效,但反序列化无效。
回应
https://pastebin.com/VhDw5Rsg(Pastebin 因为它超出了限制)
问题
我将回复粘贴到在线 converter 并且它曾经工作了一段时间,但是一旦我发表评论它就又坏了。
我该如何解决?如果反序列化失败,我也想抛出一个异常。
片段
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Ardalis.GuardClauses;
using RestSharp;
namespace QSGEngine.Web.Platforms.Binance;
/// <summary>
/// Binance REST API implementation.
/// </summary>
internal class BinanceRestApiClient : IDisposable
{
/// <summary>
/// The base point url.
/// </summary>
private const string BasePointUrl = "https://api.binance.com";
/// <summary>
/// The key header.
/// </summary>
private const string KeyHeader = "X-MBX-APIKEY";
/// <summary>
/// REST Client.
/// </summary>
private readonly IRestClient _restClient = new RestClient(BasePointUrl);
/// <summary>
/// Initializes a new instance of the <see cref="BinanceRestApiClient"/> class.
/// </summary>
/// <param name="apiKey">Binance API key.</param>
/// <param name="apiSecret">Binance Secret key.</param>
public BinanceRestApiClient(string apiKey, string apiSecret)
{
Guard.Against.NullOrWhiteSpace(apiKey, nameof(apiKey));
Guard.Against.NullOrWhiteSpace(apiSecret, nameof(apiSecret));
ApiKey = apiKey;
ApiSecret = apiSecret;
}
/// <summary>
/// The API key.
/// </summary>
public string ApiKey { get; }
/// <summary>
/// The secret key.
/// </summary>
public string ApiSecret { get; }
/// <summary>
/// Gets the total account cash balance for specified account type.
/// </summary>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public AccountInformation? GetBalances()
{
var queryString = $"timestamp={GetNonce()}";
var endpoint = $"/api/v3/account?{queryString}&signature={AuthenticationToken(queryString)}";
var request = new RestRequest(endpoint, Method.GET);
request.AddHeader(KeyHeader, ApiKey);
var response = ExecuteRestRequest(request);
if (response.StatusCode != HttpStatusCode.OK)
{
throw new Exception($"{nameof(BinanceRestApiClient)}: request failed: [{(int)response.StatusCode}] {response.StatusDescription}, Content: {response.Content}, ErrorMessage: {response.ErrorMessage}");
}
var deserialize = JsonSerializer.Deserialize<AccountInformation>(response.Content);
return deserialize;
}
/// <summary>
/// If an IP address exceeds a certain number of requests per minute
/// HTTP 429 return code is used when breaking a request rate limit.
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private IRestResponse ExecuteRestRequest(IRestRequest request)
{
const int maxAttempts = 10;
var attempts = 0;
IRestResponse response;
do
{
// TODO: RateLimiter
//if (!_restRateLimiter.WaitToProceed(TimeSpan.Zero))
//{
// Log.Trace("Brokerage.OnMessage(): " + new BrokerageMessageEvent(BrokerageMessageType.Warning, "RateLimit",
// "The API request has been rate limited. To avoid this message, please reduce the frequency of API calls."));
// _restRateLimiter.WaitToProceed();
//}
response = _restClient.Execute(request);
// 429 status code: Too Many Requests
} while (++attempts < maxAttempts && (int)response.StatusCode == 429);
return response;
}
/// <summary>
/// Timestamp in milliseconds.
/// </summary>
/// <returns>The current timestamp in milliseconds.</returns>
private long GetNonce()
{
return DateTimeOffset.Now.ToUnixTimeMilliseconds();
}
/// <summary>
/// Creates a signature for signed endpoints.
/// </summary>
/// <param name="payload">The body of the request.</param>
/// <returns>A token representing the request params.</returns>
private string AuthenticationToken(string payload)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(ApiSecret));
var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
return BitConverter.ToString(computedHash).Replace("-", "").ToLowerInvariant();
}
/// <summary>
/// The standard dispose destructor.
/// </summary>
~BinanceRestApiClient() => Dispose(false);
/// <summary>
/// Returns true if it is already disposed.
/// </summary>
public bool IsDisposed { get; private set; }
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <param name="disposing">If this method is called by a user's code.</param>
private void Dispose(bool disposing)
{
if (IsDisposed) return;
if (disposing)
{
}
IsDisposed = true;
}
/// <summary>
/// Throw if disposed.
/// </summary>
/// <exception cref="ObjectDisposedException"></exception>
private void ThrowIfDisposed()
{
if (IsDisposed)
{
throw new ObjectDisposedException("BinanceRestClient has been disposed.");
}
}
}
namespace QSGEngine.Web.Platforms.Binance;
/// <summary>
/// Information about the account.
/// </summary>
public class AccountInformation
{
/// <summary>
/// Commission percentage to pay when making trades.
/// </summary>
public decimal MakerCommission { get; set; }
/// <summary>
/// Commission percentage to pay when taking trades.
/// </summary>
public decimal TakerCommission { get; set; }
/// <summary>
/// Commission percentage to buy when buying.
/// </summary>
public decimal BuyerCommission { get; set; }
/// <summary>
/// Commission percentage to buy when selling.
/// </summary>
public decimal SellerCommission { get; set; }
/// <summary>
/// Boolean indicating if this account can trade.
/// </summary>
public bool CanTrade { get; set; }
/// <summary>
/// Boolean indicating if this account can withdraw.
/// </summary>
public bool CanWithdraw { get; set; }
/// <summary>
/// Boolean indicating if this account can deposit.
/// </summary>
public bool CanDeposit { get; set; }
/// <summary>
/// The time of the update.
/// </summary>
//[JsonConverter(typeof(TimestampConverter))]
public long UpdateTime { get; set; }
/// <summary>
/// The type of the account.
/// </summary>
public string AccountType { get; set; }
/// <summary>
/// List of assets with their current balances.
/// </summary>
public IEnumerable<Balance> Balances { get; set; }
/// <summary>
/// Permission types.
/// </summary>
public IEnumerable<string> Permissions { get; set; }
}
/// <summary>
/// Information about an asset balance.
/// </summary>
public class Balance
{
/// <summary>
/// The asset this balance is for.
/// </summary>
public string Asset { get; set; }
/// <summary>
/// The amount that isn't locked in a trade.
/// </summary>
public decimal Free { get; set; }
/// <summary>
/// The amount that is currently locked in a trade.
/// </summary>
public decimal Locked { get; set; }
/// <summary>
/// The total balance of this asset (Free + Locked).
/// </summary>
public decimal Total => Free + Locked;
}
System.Text.Json 的实现方式与 Newtonsoft.Json 完全不同。它首先被编写为一个非常快速的(反)序列化器,并尽可能地做到无分配。
但是,它也有其自身的一系列限制,其中一个限制是开箱即用,它支持的内容更加严格。
让我们看看你的 JSON:
{"makerCommission":10,"takerCommission":10,"buyerCommission":0,
"sellerCommission":0,"canTrade":true,"canWithdraw":true,"canDeposit":true,
"updateTime":1636983729026,"accountType":"SPOT",
"balances":[{"asset":"BTC","free":"0.00000000","locked":"0.00000000"},
{"asset":"LTC","free":"0.00000000","locked":"0.00000000"},
(出于示例目的重新格式化和剪切)
这里有两个问题需要解决:
- JSON中的属性是用小写首字母写的。这将根本不匹配开箱即用的 .NET 类型中的属性。
free
和 locked
的值是 JSON 中的字符串,但在您的 .NET 类型中键入为 decimal
。
要解决这些问题,您的反序列化代码需要告诉 System.Text.Json 如何处理它们,方法如下:
var options = new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString
};
然后你通过反序列化的方式传入这个对象,像这样:
… = JsonSerializer.Deserialize<AccountInformation>(response.Content, options);
这应该正确地将此内容反序列化到您的对象中。
我正在尝试实现以下 API Endpoint。由于 System.Text.Json
现在比 Newtonsoft.Json 更受欢迎,我决定尝试一下。响应显然有效,但反序列化无效。
回应
https://pastebin.com/VhDw5Rsg(Pastebin 因为它超出了限制)
问题
我将回复粘贴到在线 converter 并且它曾经工作了一段时间,但是一旦我发表评论它就又坏了。
我该如何解决?如果反序列化失败,我也想抛出一个异常。
片段
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Ardalis.GuardClauses;
using RestSharp;
namespace QSGEngine.Web.Platforms.Binance;
/// <summary>
/// Binance REST API implementation.
/// </summary>
internal class BinanceRestApiClient : IDisposable
{
/// <summary>
/// The base point url.
/// </summary>
private const string BasePointUrl = "https://api.binance.com";
/// <summary>
/// The key header.
/// </summary>
private const string KeyHeader = "X-MBX-APIKEY";
/// <summary>
/// REST Client.
/// </summary>
private readonly IRestClient _restClient = new RestClient(BasePointUrl);
/// <summary>
/// Initializes a new instance of the <see cref="BinanceRestApiClient"/> class.
/// </summary>
/// <param name="apiKey">Binance API key.</param>
/// <param name="apiSecret">Binance Secret key.</param>
public BinanceRestApiClient(string apiKey, string apiSecret)
{
Guard.Against.NullOrWhiteSpace(apiKey, nameof(apiKey));
Guard.Against.NullOrWhiteSpace(apiSecret, nameof(apiSecret));
ApiKey = apiKey;
ApiSecret = apiSecret;
}
/// <summary>
/// The API key.
/// </summary>
public string ApiKey { get; }
/// <summary>
/// The secret key.
/// </summary>
public string ApiSecret { get; }
/// <summary>
/// Gets the total account cash balance for specified account type.
/// </summary>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public AccountInformation? GetBalances()
{
var queryString = $"timestamp={GetNonce()}";
var endpoint = $"/api/v3/account?{queryString}&signature={AuthenticationToken(queryString)}";
var request = new RestRequest(endpoint, Method.GET);
request.AddHeader(KeyHeader, ApiKey);
var response = ExecuteRestRequest(request);
if (response.StatusCode != HttpStatusCode.OK)
{
throw new Exception($"{nameof(BinanceRestApiClient)}: request failed: [{(int)response.StatusCode}] {response.StatusDescription}, Content: {response.Content}, ErrorMessage: {response.ErrorMessage}");
}
var deserialize = JsonSerializer.Deserialize<AccountInformation>(response.Content);
return deserialize;
}
/// <summary>
/// If an IP address exceeds a certain number of requests per minute
/// HTTP 429 return code is used when breaking a request rate limit.
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private IRestResponse ExecuteRestRequest(IRestRequest request)
{
const int maxAttempts = 10;
var attempts = 0;
IRestResponse response;
do
{
// TODO: RateLimiter
//if (!_restRateLimiter.WaitToProceed(TimeSpan.Zero))
//{
// Log.Trace("Brokerage.OnMessage(): " + new BrokerageMessageEvent(BrokerageMessageType.Warning, "RateLimit",
// "The API request has been rate limited. To avoid this message, please reduce the frequency of API calls."));
// _restRateLimiter.WaitToProceed();
//}
response = _restClient.Execute(request);
// 429 status code: Too Many Requests
} while (++attempts < maxAttempts && (int)response.StatusCode == 429);
return response;
}
/// <summary>
/// Timestamp in milliseconds.
/// </summary>
/// <returns>The current timestamp in milliseconds.</returns>
private long GetNonce()
{
return DateTimeOffset.Now.ToUnixTimeMilliseconds();
}
/// <summary>
/// Creates a signature for signed endpoints.
/// </summary>
/// <param name="payload">The body of the request.</param>
/// <returns>A token representing the request params.</returns>
private string AuthenticationToken(string payload)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(ApiSecret));
var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
return BitConverter.ToString(computedHash).Replace("-", "").ToLowerInvariant();
}
/// <summary>
/// The standard dispose destructor.
/// </summary>
~BinanceRestApiClient() => Dispose(false);
/// <summary>
/// Returns true if it is already disposed.
/// </summary>
public bool IsDisposed { get; private set; }
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
/// <param name="disposing">If this method is called by a user's code.</param>
private void Dispose(bool disposing)
{
if (IsDisposed) return;
if (disposing)
{
}
IsDisposed = true;
}
/// <summary>
/// Throw if disposed.
/// </summary>
/// <exception cref="ObjectDisposedException"></exception>
private void ThrowIfDisposed()
{
if (IsDisposed)
{
throw new ObjectDisposedException("BinanceRestClient has been disposed.");
}
}
}
namespace QSGEngine.Web.Platforms.Binance;
/// <summary>
/// Information about the account.
/// </summary>
public class AccountInformation
{
/// <summary>
/// Commission percentage to pay when making trades.
/// </summary>
public decimal MakerCommission { get; set; }
/// <summary>
/// Commission percentage to pay when taking trades.
/// </summary>
public decimal TakerCommission { get; set; }
/// <summary>
/// Commission percentage to buy when buying.
/// </summary>
public decimal BuyerCommission { get; set; }
/// <summary>
/// Commission percentage to buy when selling.
/// </summary>
public decimal SellerCommission { get; set; }
/// <summary>
/// Boolean indicating if this account can trade.
/// </summary>
public bool CanTrade { get; set; }
/// <summary>
/// Boolean indicating if this account can withdraw.
/// </summary>
public bool CanWithdraw { get; set; }
/// <summary>
/// Boolean indicating if this account can deposit.
/// </summary>
public bool CanDeposit { get; set; }
/// <summary>
/// The time of the update.
/// </summary>
//[JsonConverter(typeof(TimestampConverter))]
public long UpdateTime { get; set; }
/// <summary>
/// The type of the account.
/// </summary>
public string AccountType { get; set; }
/// <summary>
/// List of assets with their current balances.
/// </summary>
public IEnumerable<Balance> Balances { get; set; }
/// <summary>
/// Permission types.
/// </summary>
public IEnumerable<string> Permissions { get; set; }
}
/// <summary>
/// Information about an asset balance.
/// </summary>
public class Balance
{
/// <summary>
/// The asset this balance is for.
/// </summary>
public string Asset { get; set; }
/// <summary>
/// The amount that isn't locked in a trade.
/// </summary>
public decimal Free { get; set; }
/// <summary>
/// The amount that is currently locked in a trade.
/// </summary>
public decimal Locked { get; set; }
/// <summary>
/// The total balance of this asset (Free + Locked).
/// </summary>
public decimal Total => Free + Locked;
}
System.Text.Json 的实现方式与 Newtonsoft.Json 完全不同。它首先被编写为一个非常快速的(反)序列化器,并尽可能地做到无分配。
但是,它也有其自身的一系列限制,其中一个限制是开箱即用,它支持的内容更加严格。
让我们看看你的 JSON:
{"makerCommission":10,"takerCommission":10,"buyerCommission":0,
"sellerCommission":0,"canTrade":true,"canWithdraw":true,"canDeposit":true,
"updateTime":1636983729026,"accountType":"SPOT",
"balances":[{"asset":"BTC","free":"0.00000000","locked":"0.00000000"},
{"asset":"LTC","free":"0.00000000","locked":"0.00000000"},
(出于示例目的重新格式化和剪切)
这里有两个问题需要解决:
- JSON中的属性是用小写首字母写的。这将根本不匹配开箱即用的 .NET 类型中的属性。
free
和locked
的值是 JSON 中的字符串,但在您的 .NET 类型中键入为decimal
。
要解决这些问题,您的反序列化代码需要告诉 System.Text.Json 如何处理它们,方法如下:
var options = new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
NumberHandling = JsonNumberHandling.AllowReadingFromString
};
然后你通过反序列化的方式传入这个对象,像这样:
… = JsonSerializer.Deserialize<AccountInformation>(response.Content, options);
这应该正确地将此内容反序列化到您的对象中。