【问题标题】:Newtonsoft JSON/JSON.NET custom serialization - System.Object members not correctly deserialized even when setting TypeNameHandlingNewtonsoft JSON/JSON.NET 自定义序列化 - System.Object 成员即使在设置 TypeNameHandling 时也未正确反序列化
【发布时间】:2019-06-28 13:45:14
【问题描述】:

我正在尝试使用 Newtonsoft.Json 实现自定义序列化,我希望将所有字段序列化,然后反序列化为正确的类型。该类包含“object”类型的字段,实现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 实例)上得到一个正确类型的对象,但是,任何具有默认值的非序列化字段都被初始化为 null 而不是它们的默认值(如字段 Foobar.someOtherObject )。
  3. 删除Serializable 属性。那么ISerializable GetObjectData和序列化构造函数就不会被调用。
  4. 将 JsonSerializer 的 TypeNameHandling 属性设置为 All(无效)。

那么 - 关于如何解决这个问题的任何提示?

【问题讨论】:

  • 默认情况下,Json.Net 将序列化所有公共属性。如果向字段添加 [JsonProperty] 属性,则可以使其序列化/反序列化特定的私有字段。看起来您没有在序列化构造函数或GetObjectData 中执行任何自定义操作,所以除非我遗漏了什么,否则我认为您不需要实现ISerializable 也不需要使用[Serializable] 属性。如果您只是将 Foobar 设为普通的 c# 对象,它似乎可以按预期工作。见dotnetfiddle.net/0e1uGM。我错过了什么吗?
  • 我的实际用例是序列化和反序列化一个相当庞大而复杂的数据结构,其中我需要序列化的不一定只是公开的。它是遗留代码,并且存在的字段和属性的数量使得将 [JsonPropery] 属性添加到我需要序列化的每个字段是不切实际的。将它们全部制作成适当的公共属性也是不切实际的。所以我想我会序列化所有字段(我通过在 GetObjectData/ctor 中的反射获取所有字段来做到这一点)。它可以工作 99%。但是,我的问题中的具体情况并非如此。

标签: c# types json.net json-serialization


【解决方案1】:

所以,总结一下你的问题/cmets:

  1. 您有一个庞大而复杂的遗留数据结构,您希望对其进行序列化和反序列化。
  2. 您要反序列化/序列化的大部分数据都位于私有字段中,向这些字段添加属性或添加公共属性太麻烦了。
  3. 其中一些字段的类型为 object,您希望为往返保留这些值的类型。
  4. 其中一些字段被明确标记为出于序列化目的而忽略,并且它们具有您希望保留的默认值。
  5. 您尝试使用TypeNameHandling.Auto 并实现ISerializable;这适用于序列化(子对象类型已写入 JSON),但不适用于反序列化(您得到的是 JObject,而不是实际的子对象实例)。
  6. 您尝试使用TypeNameHandling.Auto 并用MemberSerialization.Fields 标记您的外部类;这适用于序列化,但在反序列化时,您会丢失忽略字段的默认值。

让我们看看这两种方法,看看我们能做什么。

ISerializable

Json.Net 不尊重ISerializable 的子对象的反序列化设置TypeNameHandling.Auto 确实有点奇怪,当它确实将这些对象的类型信息写入序列化的 JSON 时。我不知道这是否是一个错误、一个疏忽,或者这里是否存在一些技术限制。无论如何,它的行为并不像预期的那样。

但是,您可以实施一种解决方法。由于您确实在SerializationInfo 中获得了JObject,并且JObject 中包含$type 字符串,因此您可以创建一个扩展方法,该方法将基于@ 从JObject 创建您的子对象987654335@:

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

然后,只要类型为object,就在序列化构造函数中使用它来代替GetValue

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();

【讨论】:

  • 首先 - 你完美地总结了这个问题。这确实可以解决我的问题!感谢您对我的问题非常有帮助和整洁的回答!由于我们的大多数类已经实现了 ISerializable,我可能会选择您在此处提供的解决方案。另一个解决方案肯定会在另一个下雨天派上用场。 :-)
  • 我知道这有点老了,但由于我自己最近一直在努力解决这个问题并且在遇到这个问题之前已经找到了解决方案,所以我想指出一个替代更改合同的方法解析器。如果您将JsonConstructorAttribute 添加到类中的构造函数,它将使用它,即使MemberSerialization 设置为Fields
猜你喜欢
  • 2016-09-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-16
  • 1970-01-01
相关资源
最近更新 更多