Preserve number precision in JSON

October 18th 2024 .NET Serialization

Recently, I was troubleshooting some unexpected behavior of a REST service my team is maintaining: in its JSON response, the same numeric field sometimes included insignificant zeros after the decimal separator and sometimes it didn't. Further research revealed that the differences originated from another JSON document which served as the data source and manifested the same behavior. But we were still wondering why the insignificant zeros in the fractional part were preserved across deserializing the value and serializing it again.

The observed behavior is best described by the following test:

[TestCase("""{"Quantity":1}""")]
[TestCase("""{"Quantity":1.0}""")]
public void DecimalPreservesPrecisionInJson(string json)
{
    var deserialized = JsonSerializer.Deserialize<DecimalQuantity>(json);
    var serialized = JsonSerializer.Serialize(deserialized);

    serialized.Should().Be(json);
}

It only happens when the value is deserialized to a decimal data type:

internal class DecimalQuantity
{
    public decimal Quantity { get; set; }
}

If it's deserialized to a double data type instead:

internal class DoubleQuantity
{
    public double Quantity { get; set; }
}

The insignificant zeros get lost during the deserialization and serialization process:

[TestCase("""{"Quantity":1}""")]
[TestCase("""{"Quantity":1.0}""")]
public void DoubleDoesNotPreservePrecisionInJson(string json)
{
    var deserialized = JsonSerializer.Deserialize<DoubleQuantity>(json);
    var serialized = JsonSerializer.Serialize(deserialized);

    serialized.Should().Be("""{"Quantity":1}""");
}

The reason for the insignificant zeros being preserved is the decimal data type. Even when initialized from a literal value in code, it preserves the insignificant zeros in the fractional part. And it even has a Scale property indicating the number of digits defined after the decimal separator:

[Test]
public void DecimalPreservesPrecision()
{
    var deserialized = new DecimalQuantity { Quantity = 1.0m };
    var serialized = JsonSerializer.Serialize(deserialized);

    deserialized.Quantity.Scale.Should().Be(1);
    serialized.Should().Be("""{"Quantity":1.0}""");
}

Of course, the JSON serializer also contributes its part to the observed behavior: it takes the Scale value into account and serializes the value with the appropriate number of digits after the decimal separator. And vice versa, it initializes a decimal value with appropriate Scale value during deserialization.

If you want to check out the behavior yourself, you can find a sample project in my GitHub repository. The tests clearly demonstrate the difference of behavior between decimal and double data types.

The fact that the decimal data type preserves even the insignificant zeros in the fractional part of the value doesn't matter in many if not most use cases. I wasn't aware of that behavior until now. However, as the JSON serialization example shows, it can make a difference in some scenarios, and it's good to be aware of it.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License