Preserve number precision in JSON
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.