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

Posted

技术标签:

【中文标题】Json.Net 两次没有以相同的方式序列化小数【英文标题】:Json.Net not serializing decimals the same way twice 【发布时间】:2020-06-12 02:11:20 【问题描述】:

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

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&lt;T&gt; 并有一个JsonObjectAttribute 允许它作为一个对象序列化,包括它的属性以及它的内部列表。 Cartdecimal 属性不会改变,但内部列表/数组中的对象及其内部对象的某些 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 实现,但我构建了它的基本模型并且无法重现结果。最明显的区别是我的基本版本丢失了抽象基类和接口的一些额外继承/实现以及这些的一些泛型类型使用(其中泛型类型参数定义了一些嵌套子对象的类型)。

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


更新

这是一个可重现的示例。我以为我已经排除了JsonConverter 但错了。因为我的内部_items 列表是在接口上键入的,所以我必须告诉 Json.NET 要反序列化回哪个具体类型。我不想要 JSON 中的实际 Type 名称,所以我没有使用 TypeNameHandling.Auto,而是为项目提供了唯一的字符串标识符属性。 JsonConverter 使用它来选择要创建的具体类型,但我猜JObject 已经将我的decimals 解析为doubles?这可能是我第二次实现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);

【问题讨论】:

一种可能性是您在数据模型中使用的是double 而不是decimal。请参阅:JsonConvert.PopulateObject() is not handling the decimal type data properly。 另一种可能性是,在您的代码中的某处,您有一个 JsonConverter,它将 JSON 预加载到 JToken 层次结构中,此时 JSON 浮点值被反序列化为中间 @987654355 @。为避免这种情况,您需要在转换器中或更高级别设置FloatParseHandling.Decimal,请参阅Force decimal type in class definition during serialization。但我们需要看到minimal reproducible example 才能确定我认为。 很惊讶你有任何尾随小数,因为尾随和前导零严格来说是一个字符串,除非你需要在整数后面加上 .0 来区分小数或浮点文字和整数字面意思 @mlibby - 实际上decimal 保留了有关尾随零的信息,请参阅docs:缩放因子还保留十进制数中的任何尾随零。尾随零不会影响算术或比较运算中的 Decimal 数的值。但是,如果应用了适当的格式字符串,ToString 方法可能会显示尾随零。 Json.NET 将保留 decimal 的尾随零,参见例如dotnetfiddle.net/KqfGSR @dbc 完美,你明白了!我正在使用JsonConverter,这是我怀疑问题所在,但无法重现它。结果我的转换器没有被执行,我没有在我的值上明确定义额外的 0。有时包含minimal reproducible example 并不容易,除非您知道问题出在哪里,因此感谢您的努力,而不仅仅是拒绝投票或投票结束。我已经更新了这个问题。我了解您的问题,但不确定我是否了解您的解决方案。 【参考方案1】:

如果您的多态模型包含 decimal 属性,为了不丢失精度,您必须在将 JSON 预加载到 JToken 层次结构时临时将 JsonReader.FloatParseHandling 设置为 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;
    

演示小提琴here.

为什么这是必要的?事实证明,您在 Json.NET 中遇到了一个不幸的设计决策。当JsonTextReader 遇到浮点值时,它会将其解析为上述FloatParseHandling 设置所定义的decimaldouble。一旦做出选择,就会将 JSON 值解析为目标类型并存储在JsonReader.Value 中,并丢弃底层字符序列。因此,如果浮点类型选择不当,以后就很难改正错误。

因此,理想情况下,我们希望选择“最通用”的浮点类型作为默认浮点类型,这种类型可以转换为所有其他浮点类型而不会丢失信息。不幸的是,在 .Net 中不存在这种类型Characteristics of the floating-point types中总结了这些可能性:

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

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

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

注意事项:

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

当直接反序列化为 decimaldouble 成员时,序列化程序将通过调用 ReadAsDouble()ReadAsDecimal() 覆盖默认浮点类型,因此直接从JSON 字符串。该问题仅在预加载到 JToken 层次结构然后随后反序列化时出现。

Utf8JsonReaderJsonElement 来自 system.text.json,微软在 .NET Core 3.0 中替代 Json.NET,通过始终维护浮点 JSON 值的底层字节序列来避免这个问题,这是一个例子新 API 是对旧 API 的改进。

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

【讨论】:

这是我最近读过的最好的答案之一,谢谢!这解决了我的问题,但它似乎留下了一个漏洞,因为您必须将转换器应用于父属性/容器,您可能会遇到需要DoubleDecimal 的问题。例如。如果IItem 有一个额外的double Number 属性,则FeeNumber 都将被解析为doubledecimal。因此,如果我理解正确,您最终可能不得不在精度损失或超出范围值之间做出选择? 是的,这是可能的。但实际上,在同一个多态数据模型中,您将获得大于 10^28 且精度超过 15 位 + 尾随零的值的可能性有多大?您可能可以选择其中一个,并且事情会正常工作,因为您不太可能拥有如此大且如此精确的值。但现在可能不是研究System.Text.Json的时候了。 是的,就像我说的,它解决了我的问题。只是确保我理解它。 System.Text.Json 的方法听起来更好,我迫不及待地开始使用 .NET Core。不幸的是,我们还没有完全到达那里。

以上是关于Json.Net 两次没有以相同的方式序列化小数的主要内容,如果未能解决你的问题,请参考以下文章

JSON以相同的方式序列化两个十进制数字

使用 json.net 反序列化没有类型信息的多态 json 类

使用 Json.Net 序列化哈希表

Json.Net以索引为名称反序列化JSON对象[重复]

.NET的JSON格式数据的三种转换方式

Json.net 忽略实体某些属性的序列化