Home > Mobile >  System.Text.Json - failing to deserialize a REST response
System.Text.Json - failing to deserialize a REST response

Time:11-18

I'm trying to implement the following API Endpoint. Due to the fact, System.Text.Json is now preferred over Newtonsoft.Json, I decided to try it. The response clearly works, but the deserialization doesn't.

Response

https://pastebin.com/VhDw5Rsg (Pastebin because it exceeds the limits)

Issue

I pasted the response into an online converter and it used to work for a bit, but then it broke again once I put the comments.

How do I fix it? I would also like to throw an exception if it fails to deserialize it.

Snippet

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

CodePudding user response:

System.Text.Json is implemented quite different compared to Newtonsoft.Json. It is written to be first and foremost a very fast (de)serializer, and try to be allocation-free as much as possible.

However, it also comes with its own set of limitations, and one of those limitations is that out of the box it's a bit more rigid in what it supports.

Let's look at your 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"},

(reformatted and cut for example purposes)

There's two issues here that needs to be resolved:

  1. The properties in the JSON is written with a lowercase first letter. This will simply not match the properties in your .NET Types out of the box.
  2. The values for free and locked are strings in the JSON but typed as decimal in your .NET types.

To fix these, your deserialization code needs to tell System.Text.Json how to deal with them, and this is how:

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

And then you pass this object in through the deserialization method, like this:

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

This should properly deserialize this content into your objects.

  • Related