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"},

(出于示例目的重新格式化和剪切)

这里有两个问题需要解决:

  1. JSON中的属性是用小写首字母写的。这将根本不匹配开箱即用的 .NET 类型中的属性。
  2. freelocked 的值是 JSON 中的字符串,但在您的 .NET 类型中键入为 decimal

要解决这些问题,您的反序列化代码需要告诉 System.Text.Json 如何处理它们,方法如下:

var options = new System.Text.Json.JsonSerializerOptions 
{
    PropertyNameCaseInsensitive = true,
    NumberHandling = JsonNumberHandling.AllowReadingFromString
};

然后你通过反序列化的方式传入这个对象,像这样:

… = JsonSerializer.Deserialize<AccountInformation>(response.Content, options);

这应该正确地将此内容反序列化到您的对象中。