Json.Net 两次没有以相同的方式序列化小数

Json.Net not serializing decimals the same way twice

我正在测试我正在处理的购物车的 Json.NET 序列化,并注意到当我序列化 -> 反序列化 -> 再次序列化时,我发现一些尾随零格式有所不同decimal 个字段。这是序列化代码:

private static void TestRoundTripCartSerialization(Cart cart)
{
    string cartJson = JsonConvert.SerializeObject(cart, Formatting.Indented);

    Console.WriteLine(cartJson);

    Cart cartClone = JsonConvert.DeserializeObject<Cart>(cartJson);

    string cloneJson = JsonConvert.SerializeObject(cartClone, Formatting.Indented);

    Console.WriteLine(cloneJson);

    Console.WriteLine("\r\n Serialized carts are " + (cartJson == cloneJson ? "" : "not") + " identical");
}

Cart 实现了 IEnumerable<T> 并且有一个 JsonObjectAttribute 允许它序列化为一个对象,包括它的属性和它的内部列表。 Cartdecimal 属性没有改变,但是内部 list/array 中对象及其内部对象的某些 decimal 属性与上面的代码:

第一次连载:

      ...
      "Total": 27.0000,
      "PaymentPlan": {
        "TaxRate": 8.00000,
        "ManualDiscountApplied": 0.0,
        "AdditionalCashDiscountApplied": 0.0,
        "PreTaxDeposit": 25.0000,
        "PreTaxBalance": 0.0,
        "DepositTax": 2.00,
        "BalanceTax": 0.0,
        "SNPFee": 25.0000,
        "cartItemPaymentPlanTypeID": "SNP",
        "unitPreTaxTotal": 25.0000,
        "unitTax": 2.00
      }
    }
  ],
 }

第二次连载:

      ...
      "Total": 27.0,
      "PaymentPlan": {
        "TaxRate": 8.0,
        "ManualDiscountApplied": 0.0,
        "AdditionalCashDiscountApplied": 0.0,
        "PreTaxDeposit": 25.0,
        "PreTaxBalance": 0.0,
        "DepositTax": 2.0,
        "BalanceTax": 0.0,
        "SNPFee": 25.0,
        "cartItemPaymentPlanTypeID": "SNP",
        "unitPreTaxTotal": 25.0,
        "unitTax": 2.0
      }
    }
  ],
 }

请注意 TotalTaxRate 和其他一些已从四个尾随零更改为单个尾随零。我确实在某一时刻发现了一些关于更改源代码中尾随零的处理的内容,但我对这些内容的理解还不够深入。我无法在此处分享完整的 Cart 实现,但我构建了它的基本模型并且无法重现结果。最明显的区别是我的基本版本丢失了一些额外的 inheritance/implementation 抽象基础 类 和接口以及一些通用类型的使用(其中通用类型参数定义了一些嵌套子对象的类型).

所以我希望没有那个人仍然可以回答:知道为什么尾随零会改变吗?在反序列化任一 JSON 字符串后,这些对象似乎与原始对象相同,但我想确保 Json.NET 中没有导致精度损失或舍入的东西可能会逐渐改变其中之一经过多次序列化往返后的这些小数。


已更新

这是一个可重现的例子。我以为我已经排除了 JsonConverter 但我错了。因为我的内部 _items 列表是在接口上键入的,所以我必须告诉 Json.NET 反序列化回哪个具体类型。我不想在 JSON 中使用实际的 Type 名称,所以我没有使用 TypeNameHandling.Auto,而是为项目提供了一个唯一的字符串标识符 属性。 JsonConverter 使用它来选择要创建的具体类型,但我猜 JObject 已经将我的 decimal 解析为 double 了?这可能是我第二次实现 JsonConverter 并且我不完全了解它们的工作原理,因为很难找到文档。所以我可能 ReadJson 都错了。

[JsonObject]
public class Test : IEnumerable<IItem>
{
    [JsonProperty(ItemConverterType = typeof(TestItemJsonConverter))]
    protected List<IItem> _items;

    public Test() { }

    [JsonConstructor]
    public Test(IEnumerable<IItem> o)
    {
        _items = o == null ? new List<IItem>() : new List<IItem>(o);
    }

    public decimal Total { get; set; }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return _items.GetEnumerator();
    }

    IEnumerator<IItem> IEnumerable<IItem>.GetEnumerator()
    {
        return _items.GetEnumerator();
    }
}

public interface IItem
{
    string ItemName { get; }
}

public class Item1 : IItem
{
    public Item1() { }
    public Item1(decimal fee) { Fee = fee; }

    public string ItemName { get { return "Item1"; } }

    public virtual decimal Fee { get; set; }
}

public class TestItemJsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType) { return (objectType == typeof(IItem)); }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        object result = null;

        JObject jObj = JObject.Load(reader);

        string itemTypeID = jObj["ItemName"].Value<string>();

        //NOTE: My real implementation doesn't have hard coded strings or types here.
        //See the code block below for actual implementation.
        if (itemTypeID == "Item1")
            result = jObj.ToObject(typeof(Item1), serializer);

        return result;
    }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); }
}

class Program
{
    static void Main(string[] args)
    {
        Test test1 = new Test(new List<Item1> { new Item1(9.00m), new Item1(24.0000m) })
        {
            Total = 33.0000m
        };

        string json = JsonConvert.SerializeObject(test1, Formatting.Indented);
        Console.WriteLine(json);
        Console.WriteLine();

        Test test1Clone = JsonConvert.DeserializeObject<Test>(json);
        string json2 = JsonConvert.SerializeObject(test1Clone, Formatting.Indented);
        Console.WriteLine(json2);

        Console.ReadLine();
    }
}

我的实际转换器的片段:

if (CartItemTypes.TypeMaps.ContainsKey(itemTypeID))
    result = jObj.ToObject(CartItemTypes.TypeMaps[itemTypeID], serializer);

如果您的多态模型包含 decimal 属性,为了不丢失精度,您必须在将 JSON 预加载到 JToken 中时临时设置 JsonReader.FloatParseHandling to be FloatParseHandling.Decimal层次结构,像这样:

public class TestItemJsonConverter : JsonConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        object result = null;

        var old = reader.FloatParseHandling;
        try
        {
            reader.FloatParseHandling = FloatParseHandling.Decimal;

            JObject jObj = JObject.Load(reader);
            string itemTypeID = jObj["ItemName"].Value<string>();

            //NOTE: My real implementation doesn't have hard coded strings or types here.
            //See the code block below for actual implementation.
            if (itemTypeID == "Item1")
                result = jObj.ToObject(typeof(Item1), serializer);
        }
        finally
        {
            reader.FloatParseHandling = old;
        }

        return result;
    }

演示 fiddle here.

为什么这是必要的?事实证明,您在 Json.NET 中遇到了一个不幸的设计决定。当 JsonTextReader encounters a floating-point value, it parses it to either decimal or double as defined by the above-mentioned FloatParseHandling setting. Once the choice is made, the JSON value is parsed into the target type and stored in JsonReader.Value 时,底层字符序列被丢弃。因此,如果浮点类型选择不当,以后很难纠正错误。

因此,理想情况下我们希望选择 "most general" 浮点类型作为默认浮点类型,该类型可以转换为所有其他浮点类型而不会丢失信息。不幸的是,在 .Net 中 不存在这样的类型 Characteristics of the floating-point types:

总结了可能性

如您所见,double支持更大的范围,而decimal支持更大的精度。因此,为了尽量减少数据丢失,有时需要选择 decimal,有时需要选择 double。而且,再次不幸的是,JsonReader 中没有内置这样的逻辑;没有 FloatParseHandling.Auto 选项来选择最合适的表示。

如果没有这样的选项或无法将原始浮点值作为字符串加载并稍后重新解析,您将需要使用适当的 FloatParseHandling 设置对您的转换器进行硬编码当您预加载 JToken 层次结构时,基于您的数据模型。

如果您的数据模型同时包含 doubledecimal 成员,使用 FloatParseHandling.Decimal 预加载可能会满足您的需求,因为 Json.NET 会抛出一个JsonReaderException 尝试将过大的值反序列化为 decimal(演示 fiddle here),但在尝试反序列化过于精确的值时会默默地四舍五入该值变成 double。实际上,在同一个多态数据模型中,您不可能拥有大于 10^28 且精度超过 15 位 + 尾随零的浮点值。在不太可能的情况下,通过使用 FloatParseHandling.Decimal 你会得到一个解释问题的明确异常。

备注:

  • 我不知道为什么选择 double 而不是 decimal 作为 "default default" 浮点格式。 Json.NET 最初发布于 2006;我的回忆是 decimal 当时并没有被广泛使用,所以也许这是一个从未被重新审视过的遗留选择?

  • 当直接反序列化到decimaldouble成员时,序列化器会通过调用ReadAsDouble() or ReadAsDecimal()覆盖默认的浮点类型,所以精度不是直接从 JSON 字符串反序列化时丢失。只有在预加载到 JToken 层次结构然后随后反序列化时才会出现问题。

  • Utf8JsonReader and JsonElement from ,微软在 .NET Core 3.0 中对 Json.NET 的替代,通过始终保持浮点数 JSON 的底层字节序列来避免此问题值,这是新 API 对旧版本进行改进的一个例子。

    如果在同一个多态数据模型中,您的值确实大于 10^28,精度超过 15 位 + 尾随零,则切换到这个新的序列化程序可能是一个有效的选择。