Json. Net не выполняет сериализацию десятичных знаков одинаково дважды - PullRequest
3 голосов
/ 28 февраля 2020

Я тестировал 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, чтобы позволить ему сериализоваться как объект, включая его свойства, а также его внутренний список. Свойства 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
      }
    }
  ],
 }

Второй раз сериализации:

      ...
      "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
      }
    }
  ],
 }

Обратите внимание на Total, TaxRate, а некоторые другие изменили с четырех конечных нулей на одиночный конечный ноль. В какой-то момент я нашел кое-что относительно изменений в обработке конечных нулей в исходном коде, но ничего такого, что я понял достаточно хорошо, чтобы соединить это. Я не могу поделиться полной реализацией Cart здесь, но я создал ее голую модель и не смог воспроизвести результаты. Наиболее очевидные различия заключались в том, что моя голая версия потеряла некоторое дополнительное наследование / реализацию абстрактных базовых классов и интерфейсов и некоторое использование обобщенного типа c для них (где параметр типа generi c определяет тип некоторых из вложенных дочерних объектов ).

Так что я надеюсь, что без этого кто-то еще может ответить: Есть идеи, почему меняются конечные нули? После десериализации строки JSON объекты кажутся идентичными оригиналу, но я хочу убедиться, что в Json. NET нет чего-то такого, что может привести к потере точности или округлению, которое может постепенно изменить одно из следующих значений: эти десятичные дроби после многих циклических сериализаций.


Обновлено

Вот воспроизводимый пример. Я думал, что исключил JsonConverter, но ошибся. Поскольку мой внутренний список _items напечатан на интерфейсе, я должен сказать Json. NET, к какому конкретному типу следует вернуться обратно. Я не хотел фактических Type имен в JSON, поэтому вместо использования 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);

1 Ответ

2 голосов
/ 28 февраля 2020

Если ваши модели polymorphi c содержат decimal свойства, чтобы не потерять точность, вы должны временно установить JsonReader.FloatParseHandling на FloatParseHandling.Decimal, когда предварительно загрузка вашей JSON в JToken иерархию, например так:

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;
    }

Демонстрационная скрипка здесь .

Почему это необходимо? Как выяснилось, вы столкнулись с неудачным дизайнерским решением в Json. NET. Когда JsonTextReader встречает значение с плавающей точкой, оно анализирует его как decimal или double, как определено вышеупомянутым параметром FloatParseHandling. Как только выбор сделан, значение JSON анализируется в целевой тип и сохраняется в JsonReader.Value, и основная последовательность символов отбрасывается. Таким образом, если сделан неправильный выбор типа с плавающей запятой, трудно исправить ошибку позже.

Итак, в идеале мы хотели бы выбрать в качестве типа с плавающей запятой по умолчанию «наиболее общий» плавающий тип точечный тип, который можно преобразовать во все остальные без потери информации. К сожалению, в. Net такого типа не существует . Возможности суммированы в Характеристики типов с плавающей точкой :

Characteristics of the floating-point types

Как вы можете видеть , double поддерживает больший диапазон, в то время как decimal поддерживает большую точность. Таким образом, чтобы минимизировать потерю данных, иногда нужно будет выбрать decimal, а иногда double. И, опять же, к сожалению, ни одна такая логика c не встроена в JsonReader; FloatParseHandling.Auto нет возможности выбрать наиболее подходящее представление.

В отсутствие такой опции или возможности загрузить исходное значение с плавающей запятой в виде строки и повторно проанализировать его позже, вам потребуется жестко закодировать конвертер с соответствующей настройкой FloatParseHandling, основанной на ваши модели данных при предварительной загрузке иерархии JToken.

В тех случаях, когда ваши модели данных содержат элементы double и decimal, предварительная загрузка с использованием FloatParseHandling.Decimal, скорее всего, будет соответствовать ваши потребности, потому что Json. NET будет выбрасывать JsonReaderException при попытке десериализации слишком большого значения в decimal (демонстрационная скрипка здесь ), но будет молча округлять значение, когда попытка десериализации слишком точного значения в double. С практической точки зрения маловероятно, что у вас будут значения с плавающей точкой больше 10^28 с более чем 15 цифрами точности + конечные нули в той же модели данных polymorphi c. В маловероятном случае, используя FloatParseHandling.Decimal, вы получите явное исключение, объясняющее проблему.

Примечания:

  • Я не знаю, почему double был выбран вместо decimal в качестве формата по умолчанию с плавающей запятой. Json. NET был первоначально выпущен в 2006 ; Насколько я помню, decimal тогда не использовался широко, так что, может быть, это устаревший выбор, который никогда не пересматривался?

  • При десериализации непосредственно в decimal или double член, сериализатор переопределит тип с плавающей точкой по умолчанию, вызывая ReadAsDouble() или ReadAsDecimal(), поэтому точность не теряется при десериализации непосредственно из строки JSON. Проблема возникает только при предварительной загрузке в иерархию JToken и последующей десериализации.

  • Utf8JsonReader и JsonElement из , замена Microsoft на Json. NET in. NET Core 3.0, избегайте этой проблемы, всегда поддерживая основную последовательность байтов с плавающей точкой JSON значение, которое является одним из примеров того, как новый API является усовершенствованием старого.

    Если у вас действительно есть значения больше 10^28 с более чем 15 цифрами точности + конечные нули в том же полиморфе c модель данных, переключение на этот новый сериализатор может быть допустимым вариантом.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...