Home > Back-end >  How do I add lexicographical sort in this System.Text.Json serialization way?
How do I add lexicographical sort in this System.Text.Json serialization way?

Time:06-13

I'm implementing an exchange which has private endpoints which are protected with a signature. The first code looks better but it results into an invalid signature due to the fact that it doesn't lexicographically sort it. How do I fix it? The second code works fine btw.

Broken code

public ValueTask SubscribeToPrivateAsync()
{
    var request = JsonSerializer.Serialize(new MexcSubPersonalPayload(_apiKey, _apiSecret));

    return SendAsync(request);
}

internal class MexcSubPersonalPayload
{
    private readonly string _apiSecret;

    public MexcSubPersonalPayload(string apiKey, string apiSecret)
    {
        ApiKey = apiKey;
        _apiSecret = apiSecret;
    }

    [JsonPropertyName("op")]
    [JsonPropertyOrder(1)]
    public string Operation => "sub.personal";

    [JsonPropertyName("api_key")]
    [JsonPropertyOrder(2)]
    public string ApiKey { get; }

    [JsonPropertyName("sign")]
    [JsonPropertyOrder(3)]
    public string Signature => Sign(ToString());

    [JsonPropertyName("req_time")]
    [JsonPropertyOrder(4)]
    public long RequestTime => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

    private string Sign(string payload)
    {
        if (string.IsNullOrWhiteSpace(payload))
        {
            return string.Empty;
        }

        using var md5 = MD5.Create();
        var sourceBytes = Encoding.UTF8.GetBytes(payload);
        var hash = md5.ComputeHash(sourceBytes);
        return Convert.ToHexString(hash);
    }

    public override string ToString()
    {
        return $"api_key={HttpUtility.UrlEncode(ApiKey)}&req_time={RequestTime}&op={Operation}&api_secret={HttpUtility.UrlEncode(_apiSecret)}";
    }
}

Working code

public ValueTask SubscribeToPrivateAsync()
{
    var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    var @params = new Dictionary<string, object>
    {
        { "api_key", _apiKey },
        { "req_time", now },
        { "op", "sub.personal" }
    };

    var signature = SignatureHelper.Sign(SignatureHelper.GenerateSign(@params, _apiSecret));
    var request = new PersonalRequest("sub.personal", _apiKey, signature, now);

    return SendAsync(request);
}

internal static class SignatureHelper
{
    public static string GenerateSign(IDictionary<string, object> query, string apiSecret)
    {
        // Lexicographic order
        query = new SortedDictionary<string, object>(query, StringComparer.Ordinal);

        var sb = new StringBuilder();

        var queryParameterString = string.Join("&",
            query.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value.ToString()))
                .Select(kvp => $"{kvp.Key}={HttpUtility.UrlEncode(kvp.Value.ToString())}"));
        sb.Append(queryParameterString);

        if (sb.Length > 0)
        {
            sb.Append('&');
        }

        sb.Append("api_secret=").Append(HttpUtility.UrlEncode(apiSecret));

        return sb.ToString();
    }

    public static string Sign(string source)
    {
        using var md5 = MD5.Create();
        var sourceBytes = Encoding.UTF8.GetBytes(source);
        var hash = md5.ComputeHash(sourceBytes);
        return Convert.ToHexString(hash);
    }
}

CodePudding user response:

As explained in Configure the order of serialized properties, as of .NET 6 the only mechanism that System.Text.Json provides to control property order during serialization is to apply [JsonPropertyOrder] attributes to your model [1]. You have already done so -- but the order specified is not lexicographic. So what are your options?

Firstly, you could modify your model so that the [JsonPropertyOrder] are in lexicographic order:

internal class MexcSubPersonalPayload
{
    private readonly string _apiSecret;

    public MexcSubPersonalPayload(string apiKey, string apiSecret)
    {
        ApiKey = apiKey;
        _apiSecret = apiSecret;
    }

    [JsonPropertyName("op")]
    [JsonPropertyOrder(2)]
    public string Operation => "sub.personal";

    [JsonPropertyName("api_key")]
    [JsonPropertyOrder(1)]
    public string ApiKey { get; }

    [JsonPropertyName("sign")]
    [JsonPropertyOrder(4)]
    public string Signature => Sign(ToString());

    [JsonPropertyName("req_time")]
    [JsonPropertyOrder(3)]
    public long RequestTime => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

    // Remainder unchanged

Demo fiddle #1 here.

Secondly, if you cannot modify your [JsonPropertyOrder] attributes (because e.g. you require a different order in a different context), then as long as you are using .NET 6 or later, you could serialize your model to an intermediate JsonNode hierarchy and sort its properties before finally formatting as a JSON string. To do this, first introduce the following extension methods:

public static partial class JsonExtensions
{
    public static JsonNode? SortProperties(this JsonNode? node, bool recursive = true) => node.SortProperties(StringComparer.Ordinal, recursive);

    public static JsonNode? SortProperties(this JsonNode? node, IComparer<string> comparer, bool recursive = true)
    {
        if (node is JsonObject obj)
        {
            var properties = obj.ToList();
            obj.Clear();
            foreach (var pair in properties.OrderBy(p => p.Key, comparer))
                obj.Add(new (pair.Key, recursive ? pair.Value.SortProperties(comparer, recursive) : pair.Value));
        }
        else if (node is JsonArray array)
        {
            foreach (var n in array)
                n.SortProperties(comparer, recursive);
        }
        return node;
    }
}

And then you will be able to do:

public string GenerateRequest() => JsonSerializer.SerializeToNode(new MexcSubPersonalPayload(_apiKey, _apiSecret))
    .SortProperties(StringComparer.Ordinal)!
    .ToJsonString();

Demo fiddle #2 here.

Thirdly, you could create a custom JsonConverter<MexcSubPersonalPayload> that serializes the properties in the correct order, e.g. by mapping MexcSubPersonalPayload to a DTO, then serializing the DTO. First define:

class MexcSubPersonalPayloadConverter : JsonConverter<MexcSubPersonalPayload>
{
    record MexcSubPersonalPayloadDTO(
        [property:JsonPropertyName("api_key"), JsonPropertyOrder(1)] string ApiKey, 
        [property:JsonPropertyName("op"), JsonPropertyOrder(2)] string Operation, 
        [property:JsonPropertyName("req_time"), JsonPropertyOrder(3)]long RequestTime, 
        [property:JsonPropertyName("sign"), JsonPropertyOrder(4)]string Signature);

    public override void Write(Utf8JsonWriter writer, MexcSubPersonalPayload value, JsonSerializerOptions options) =>
        JsonSerializer.Serialize(writer, new MexcSubPersonalPayloadDTO(value.ApiKey, value.Operation, value.RequestTime, value.Signature), options);
    public override MexcSubPersonalPayload Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException();
}

And then do:

public string GenerateRequest() => JsonSerializer.Serialize(new MexcSubPersonalPayload(_apiKey, _apiSecret), new JsonSerializerOptions { Converters = { new MexcSubPersonalPayloadConverter() }});

Demo fiddle #3 here.


[1] With Json.NET one could override property order via a custom contract resolver, but as of .NET 6 System.Text.Json lacks this flexibility as its contract information is private.

  • Related