JamesNK / Newtonsoft.Json

Json.NET is a popular high-performance JSON framework for .NET
https://www.newtonsoft.com/json
MIT License
10.71k stars 3.24k forks source link

FloatParseHandling for Serialising data #2905

Closed samuelcavendish closed 11 months ago

samuelcavendish commented 11 months ago

When running the following Fiddle https://dotnetfiddle.net/U8DOMu you can see in the output that the Lng value has been changed and an extra decimal place has been added.

Looking at the code it seems the FloatParseHandling changes how values are read but isn't used for writing data

Fiddle code:

using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

public class LatLng
{
    public double Lat { get; set; }

    public double Lng { get; set; }
}

public class Program
{
    public static void Main()
    {
        var item = new LatLng{Lat = 52.585235595703125, Lng = -0.4142917990684509};
        Console.WriteLine(JsonConvert.SerializeObject(item, new JsonSerializerSettings{FloatParseHandling = FloatParseHandling.Decimal, Formatting = Formatting.Indented, Converters = new JsonConverter[]{new StringEnumConverter()}}));
    }
}
elgonzo commented 11 months ago

No, not a bug in Newtonsoft.Json.

Newtonsoft.Json is not changing the value here, and there are not really "decimal places added". You are just "victim" of the inherent imprecision of IEEE 754 floating point values. -0.4142917990684509 cannot be precisely stored as a IEEE 754 double (64-bit) floating point value. The nearest possible value that can be represented by a double is close to -0.414291799068450927734375 (it's still possibly not the exact base10 number, as the binary representation of a floating point number is not just a bit sequence of a single number but made of 3 distinct components - sign, base2 fraction and base2 exponent). In other words, item.Lng does not - it cannot - really have a value of -0.4142917990684509.

This behavior is inherent to how the float/double types work in .NET and how the C# compiler processes floating point literals in source code, and you can't avoid this kind of imprecision unless you avoid using float/double (and instead use something like decimal in your source data models you want to serialize, for example).

This being the result of the C# compiler trying to convert a value literal in the source code to a value that can actually be represented by a double and not being something that is caused by Newtonsoft.Json can very easily be demonstrated by executing the following code:

double value = -0.4142917990684509;
Console.WriteLine(value.ToString("G17")); // output with 17 significant digits
Console.WriteLine(value.ToString("R")); // R format specifier is used by Newtonsoft.Json

The output of this will be, you guessed it, two times -0.41429179906845093 when this is compiled for a .NET Framework target. (Note that the behavior of the R format specifiers differs between .NET Core 3.0 and newer versus .NET versions earlier than .NET Core 3.0.)

And as you already noticed, FloatParseHandling.Decimal is a setting only affecting reading/parsing of Json text (e.g., deserialization), so there is no point nor effect in twiddling with the FloatParseHandling setting for serialization tasks...

If you don't mind the imprecision inherent to float/double and you only want to output floating point values with a specified maximum number of significant digits of your choice, create and use a JsonConverter that writes float/double in the desired way. If you target .NET versions older than .NET Core 3.0, you might want to write such a converter anyway, because Newtonsoft.Json itself uses the R format specifier for writing floating point values, and R is known to have problems regarding float/double-to-string conversion in .NET versions earlier than .NET Core 3.0. An illustrative example of such a converter for double can be seen here: https://github.com/JamesNK/Newtonsoft.Json/issues/2889#issuecomment-1695255479.

samuelcavendish commented 11 months ago

Thanks for the response, I appreciate the extra info on core 3 and below

sungam3r commented 10 months ago

@elgonzo perfect answer as always 👍