Пользовательская сериализация Newtonsoft JSON / JSON.NET - члены System.Object неправильно десериализованы даже при установке TypeNameHandling - PullRequest
0 голосов
/ 28 июня 2019

Я пытаюсь реализовать пользовательскую сериализацию, используя Newtonsoft.Json, где я хочу, чтобы все поля были сериализованы, а затем десериализованы до их соответствующих типов.Класс содержит поле типа «объект», реализует ISerializable, имеет набор атрибутов [Serializable] и имеет конструктор сериализации.Я устанавливаю значение этого поля объекта для экземпляра некоторого класса, а затем сериализую его.Я использую JsonSerializer с TypeNameHandling, установленным на TypeNameHandling.Auto.

Вот код, который я пробую:

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.IO;
using System.Runtime.Serialization;

namespace ConsoleApp1
{
    class Program
    {
        [Serializable]
        class Foobar : ISerializable
        {
            public Foobar()
            {
            }

            public Foobar(SerializationInfo info, StreamingContext context)
            {
                something = info.GetValue("something", typeof(object));
            }

            public void GetObjectData(SerializationInfo info, StreamingContext context)
            {
                info.AddValue("something", something);
            }

            public object Something { get { return something; } set { something = value; } }
            private object something;

            [JsonIgnore]
            private string someOtherObject = "foobar";
        }

        class SomeOtherClass
        {
            public string Foo { get; set; }
            public string Bar { get; set; }
        }

        static void Main(string[] args)
        {
            Foobar myObj = new Foobar();

            myObj.Something = new SomeOtherClass() { Bar = "My first", Foo = "fqwkifjwq" };

            var serializer = new Newtonsoft.Json.JsonSerializer();
            serializer.TypeNameHandling = TypeNameHandling.Auto;
            serializer.Formatting = Formatting.Indented;
            string serialized;
            using (var tw = new StringWriter())
            {
                using (var jw = new JsonTextWriter(tw))
                    serializer.Serialize(jw, myObj);

                tw.Flush();
                serialized = tw.ToString();
            }

            Foobar deserialized;
            using (var rt = new StringReader(serialized))
            using (var jsonReader = new JsonTextReader(rt))
                deserialized = serializer.Deserialize<Foobar>(jsonReader);

            Console.WriteLine("Type of deserialized.Something: " + deserialized.Something.GetType().FullName);
        }
    }
}

После десериализации поле Foobar.Something имеет видпросто Newtonsoft.Json.Linq.JObject, что НЕ то, что я хочу.Я хотел бы, чтобы это было правильно десериализовано для объекта типа SomeOtherClass.Сериализованный вывод содержит необходимую информацию:

{
  "something": {
    "$type": "ConsoleApp1.Program+SomeOtherClass, ConsoleApp1",
    "Foo": "fqwkifjwq",
    "Bar": "My first"
  }
}

Вот что я пробовал до сих пор:

  1. Используя код выше.Делает именно так, как я описал выше.
  2. Использование атрибута [JsonObject(MemberSerialization = MemberSerialization.Fields)] для объекта, который я сериализую.Затем я получаю объект правильного типа в поле Foobar.Something (экземпляр SomeOtherClass), НО, любые несериализованные поля со значением по умолчанию инициализируются в ноль вместо их значения по умолчанию (например, поле Foobar.someOtherObject).
  3. Удаление атрибута Serializable.Тогда ISerializable GetObjectData и конструктор сериализации не вызывается.
  4. Установка для свойства TypeNameHandling JsonSerializer значения All (без эффекта).

Итак, любые советы о том, какэто можно решить?

1 Ответ

1 голос
/ 29 июня 2019

Итак, чтобы подвести итог вашего вопроса / комментариев:

  1. У вас есть огромная и сложная устаревшая структура данных, которую вы хотели бы сериализовать и десериализовать.
  2. Большая часть данных, которые выхотите, чтобы de / serialize находился в закрытых полях, и слишком сложно добавлять к ним атрибуты или добавлять открытые свойства.
  3. Некоторые из этих полей имеют тип object, и вы хотите сохранить тип этих значенийв оба конца.
  4. Некоторые из этих полей специально помечены как игнорируемые для целей сериализации, и у них есть значения по умолчанию, которые вы хотели бы сохранить.
  5. Вы пытались использовать TypeNameHandling.Auto и осуществляетеISerializable;это работало для сериализации (тип дочернего объекта был записан в JSON), но не работало для десериализации (вы получили JObject вместо реального экземпляра дочернего объекта).
  6. Вы пытались использовать TypeNameHandling.Autoи маркировка вашего внешнего класса MemberSerialization.Fields;это работало для сериализации, но при десериализации вы потеряли значения по умолчанию для ваших игнорируемых полей.

Давайте рассмотрим оба подхода и посмотрим, что мы можем сделать.

ISerializable

Кажется немного странным, что Json.Net не соблюдает настройку TypeNameHandling.Auto при десериализации для дочерних объектов ISerializable, когда записывает информацию о типах для них в JSON при сериализации.Я не знаю, является ли это ошибкой, недосмотром или здесь есть какие-то технические ограничения.Несмотря на это, он не ведет себя так, как ожидалось.

Однако вы можете обойти эту проблему.Поскольку вы получаете JObject обратно в SerializationInfo, а в JObject есть строка $type, вы можете создать метод расширения, который создаст ваш дочерний объект из JObject на основе $type:

public static class SerializationInfoExtensions
{
    public static object GetObject(this SerializationInfo info, string name)
    {
        object value = info.GetValue(name, typeof(object));
        if (value is JObject)
        {
            JObject obj = (JObject)value;
            string typeName = (string)obj["$type"];
            if (typeName != null)
            {
                Type type = Type.GetType(typeName);
                if (type != null)
                {
                    value = obj.ToObject(type);
                }
            }
        }
        return value;
    }
}

Затем используйте его в конструкторе сериализации вместо GetValue всякий раз, когда типом является object:

public Foobar(SerializationInfo info, StreamingContext context)
{
    something = info.GetObject("something");
}

MemberSerialization.Fields

Похоже, что использование атрибута [JsonObject(MemberSerialization = MemberSerialization.Fields)] в основном делает то, что вы хотите, за исключением потери значений по умолчанию для некоторых из ваших игнорируемых свойств.Оказывается, что Json.Net намеренно не вызывает нормальный конструктор при использовании MemberSerialization.Fields - вместо этого он использует метод FormatterServices.GetUninitializedObject для создания полностью пустого объекта перед заполнениемполя из JSON.Это, очевидно, предотвратит инициализацию значений по умолчанию.

Чтобы обойти это, вы можете использовать пользовательский ContractResolver для замены функции создания объекта, например:

class FieldsOnlyResolver : DefaultContractResolver
{
    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        JsonObjectContract contract = base.CreateObjectContract(objectType);
        contract.DefaultCreator = () => Activator.CreateInstance(objectType, true);
        return contract;
    }
}

Примечание:Выше предполагается, что все ваши объекты будут иметь конструкторы по умолчанию (без параметров).Если это не так, вы можете добавить их в случае необходимости (они могут быть частными) или изменить преобразователь для предоставления другой функции создателя в зависимости от типа.

Чтобы использовать его, просто добавьте преобразовательна ваш JsonSerializer экземпляр:

serializer.ContractResolver = new FieldsOnlyResolver();
...