Search code examples
json.netdeserializationdecimalazure-api-managementapim

How to show the decimal value as it is when Deserializing using JSON.NET in Azure APIM?


I'm using Azure APIM policy expression to aggregate multiple responses. I have some decimal values in the response. But while Deserializing , formatting was changed as shown in the output. I want to return as in the Input.

INPUT

{
    "x1": 1.55391E4,
    "x2": 2.2173244E5,
    "x3": 1.11226E3,
    "UpdatedDateTime": "2023-01-17T20:45:51.959+08:00"
}

OUTPUT

{
    "x1": 15539.1,
    "x2": 221732.44,
    "x3": 1112.26,
    "UpdatedDateTime": "2023-01-17T20:45:51.959+08:00"
}

EXPECTED

{
    "x1": 1.55391E4,
    "x2": 2.2173244E5,
    "x3": 1.11226E3,
    "UpdatedDateTime": "2023-01-17T20:45:51.959+08:00"
}

This is my fiddle

In this sample, I have preserved the DateTimeZone with Offset. but I can't do the decimal fields (x1, x2, x3). I just wants to return as it is like input.

Please note that I'm writing this inside a policy expression, so I can't create any C# extensions or helper methods.


Solution

  • One way to force scientific notation for decimal values in a JToken hierarchy would be to replace decimal-valued JValue tokens with an appropriately formatted JRaw value:

    var settings = new JsonSerializerSettings 
    {
        // Make sure that FloatParseHandling is consistent with the later check ".Where(v => v.Value is decimal)"
        FloatParseHandling = FloatParseHandling.Decimal, 
        FloatFormatHandling = FloatFormatHandling.DefaultValue, 
        // Instead of DateParseHandling.DateTimeOffset, you could use DateParseHandling.None to skip DateTime recognition and leave date/time strings unchanged.
        DateParseHandling = DateParseHandling.DateTimeOffset, 
        DateTimeZoneHandling = DateTimeZoneHandling.Unspecified
    };
    
    var obj = JsonConvert.DeserializeObject<JObject>(json, settings);
    
    var decimalValues = obj.Descendants().OfType<JValue>().Where(v => v.Value is decimal).ToList(); 
    foreach (var value in decimalValues)
    {
        value.Replace(new JRaw(((decimal)value.Value).ToString("0.00000E0" /*, System.Globalization.CultureInfo.InvariantCulture */))); // Is System.Globalization.CultureInfo.InvariantCulture available?
    }
    
    var newJson = obj.ToString(Formatting.Indented);
    

    Which results in

    {
      "x1": 1.55391E4,
      "x2": 2.21732E5,
      "x3": 1.11226E3,
      "UpdatedDateTime": "2023-01-17T20:45:51.959+08:00"
    }
    

    Demo fiddle #1 here.

    Notes:

    • You code is inside an Azure APIM policy expression. Only a very limited set of types are allowed in such an expression, as documented in .NET Framework types allowed in policy expressions. Of note, the following are not available:

      • Newtonsoft.Json.JsonConverter.
      • Newtonsoft.Json.JsonTextReader and JsonTextWriter.
      • System.Text.Json (all).

      The lack of any ability to create a custom converter is why I suggested to use JRaw.

    • You can't preserve the original decimal formatting with Json.NET. When JsonTextReader encounters a floating point JSON number, it parses it to decimal or double and discards the original JSON character sequence. Thus only the value (and number of digits in the case of decimal) is retained.

    • Utf8JsonReader from System.Text.Json, on the other hand, does retain the underlying JSON character sequence. This character sequence is passed off to JsonElement and JsonNode which also retain the original character sequence and present read-only (or editable) views of it. So if Azure APIM policy expressions are ever enhanced to allow System.Text.Json you would be able to retain the original JSON decimal formatting much more easily.

      Demo fiddle #2 here.

    • If you want to leave all date & time string values unchanged, instead of DateParseHandling.DateTimeOffset, you could use DateParseHandling.None to disable DateTime recognition entirely.