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

xr2*_*0xr 4 c# decimal json.net

我正在测试我正在处理的购物车的 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");
}
Run Code Online (Sandbox Code Playgroud)

Cart器具IEnumerable<T>,并且具有JsonObjectAttribute允许其序列作为对象,包括其属性以及其内部列表。的decimal属性Cart不会改变,但是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
      }
    }
  ],
 }
Run Code Online (Sandbox Code Playgroud)

第二次连载:

      ...
      "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
      }
    }
  ],
 }
Run Code Online (Sandbox Code Playgroud)

请注意TotalTaxRate和其他一些人也从四个尾随零置换为单尾随零。我确实找到了一些关于更改源代码中尾随零处理的内容,但没有任何我理解得足以将其放在一起的内容。我不能在这里分享完整的 Cart 实现,但我构建了它的基本模型并且无法重现结果。最明显的区别是我的裸机版本丢失了抽象基类和接口的一些额外继承/实现以及它们的一些泛型类型用法(其中泛型类型参数定义了一些嵌套子对象的类型)。

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


更新

这是一个可重现的示例。我以为我已经排除了,JsonConverter但错了。因为我的内部_items列表是在接口上键入的,所以我必须告诉 Json.NET 要反序列化回哪个具体类型。我不想要TypeJSON 中的实际名称,因此我没有使用TypeNameHandling.Auto,而是为项目提供了唯一的字符串标识符属性。在JsonConverter该选择一个具体的类型来创建的,但我想用途JObject已经解析我decimals到doubleS' 这可能是我第二次实施 aJsonConverter并且我对它们的工作方式没有完全了解,因为查找文档很困难。所以我可能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();
    }
}
Run Code Online (Sandbox Code Playgroud)

来自我的实际转换器的片段:

if (CartItemTypes.TypeMaps.ContainsKey(itemTypeID))
    result = jObj.ToObject(CartItemTypes.TypeMaps[itemTypeID], serializer);
Run Code Online (Sandbox Code Playgroud)

dbc*_*dbc 7

如果您的多态模型包含decimal属性,为了不丢失精度,您必须在将 JSON 预加载到层次结构中时临时设置JsonReader.FloatParseHandling为,如下所示:FloatParseHandling.DecimalJToken

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;
    }
Run Code Online (Sandbox Code Playgroud)

演示小提琴在这里

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

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

浮点类型的特征

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

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

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

笔记:

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

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

  • Utf8JsonReaderJsonElement,微软的替代Json.NET在.NET 3.0的核心,通过始终保持浮点值JSON的基本字节序列,这是新的API是一个改进的一个例子避免这个问题老人。

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