【问题标题】:System.Text.Json - failing to deserialize a REST responseSystem.Text.Json - 无法反序列化 REST 响应
【发布时间】:2021-11-17 13:17:35
【问题描述】:

我正在尝试实现以下API Endpoint。由于事实上,System.Text.Json 现在比 Newtonsoft.Json 更受欢迎,我决定尝试一下。响应显然有效,但反序列化无效。

响应

https://pastebin.com/VhDw5Rsg(Pastebin,因为它超出了限制)

问题

我将回复粘贴到在线converter 中,它曾经可以工作一段时间,但在我放入 cmets 后它又坏了。

我该如何解决?如果反序列化失败,我也想抛出异常。

片段

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;
}

【问题讨论】:

  • 相对于您的代码,您的 JSON 存在两个问题,当使用 System.Text.Json.First 时,JSON 中的属性以小写首字母拼写。为了让 System.Text.Json 处理这个问题,您需要使用PropertyNameCaseInsensitive = true 传入一个选项对象。此外,JSON 中的数字被写成字符串,这不适用于 System.Text.Json 开箱即用。您需要为此添加转换器。有关示例,请参见 stackoverflow.com/questions/59097784/…
  • 您可以尝试将此选项对象传递给反序列化:var options = new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString };

标签: c# asp.net-core system.text.json


【解决方案1】:

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);

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

【讨论】:

  • 谢谢!很好的解释! TimestampConverter 呢,有内置的还是我自己创建一个?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-07-17
  • 2021-05-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多