【问题标题】:Json.Net not deserializing references dynamicallyJson.Net 没有动态反序列化引用
【发布时间】:2016-09-07 15:34:13
【问题描述】:

我有一个带有循环引用的大型对象图,我正在使用 Json.Net 对其进行序列化,以便在发送到客户端之前保留这些引用。在客户端,我使用了 Ken Smith 的JsonNetDecycle 的定制版本,这反过来又基于 Douglas Crockford 的cycle.js 在反序列化时恢复循环对象引用,并在将对象发回之前再次删除它们到服务器。在服务器端,我使用了一个自定义的 JsonDotNetValueProvider,类似于 this 问题中的那个,以便使用 Json.Net 而不是库存的 MVC5 JavaScriptSerializer。从服务器到客户端再返回,一切似乎都运行良好,Json 在往返过程中正常运行,但 MVC 无法正确反序列化对象图。

我已将问题追溯到此。当我将 JsonConvert.Deserialize 与具体类型参数一起使用时,一切正常,并且我得到了一个完整的对象图,其中子级和兄弟级正确地相互引用。但是,这不适用于 MVC ValueProvider,因为您不知道生命周期中那个时候的模型类型。 ValueProvider 只是应该以字典的形式提供值供 ModelBinder 使用。

在我看来,除非您可以为反序列化提供具体类型,否则对图中任何给定对象的第一次引用都可以反序列化,但对同一对象的任何后续引用都不会。那里有一个对象,但没有填写任何属性。

为了演示,我已经创建了最小的问题演示。在这个类中(使用 Json.Net 和 NUnit),我创建了一个对象图,并尝试以三种不同的方式对其进行反序列化。查看其他内联 cmets。

using System.Collections.Generic;
using System.Dynamic;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using NUnit.Framework;

namespace JsonDotNetSerialization
{
[TestFixture]
public class When_serializing_and_deserializing_a_complex_graph
{
    public Dude TheDude;
    public Dude Gramps { get; set; }
    public string Json { get; set; }

    public class Dude
    {
        public List<Dude> Bros { get; set; }
        public string Name { get; set; }
        public Dude OldMan { get; set; }
        public List<Dude> Sons { get; set; }

        public Dude()
        {
            Bros = new List<Dude>();
            Sons = new List<Dude>();
        }
    }

    [SetUp]
    public void SetUp()
    {
        Gramps = new Dude
        {
            Name = "Gramps"
        };

        TheDude = new Dude
        {
            Name = "The Dude",
            OldMan = Gramps
        };

        var son1 = new Dude {Name = "Number one son", OldMan = TheDude};
        var son2 = new Dude {Name = "Lil' Bro", OldMan = TheDude, Bros = new List<Dude> {son1}};
        son1.Bros = new List<Dude> {son2};
        TheDude.Sons = new List<Dude> {son1, son2};
        Gramps.Sons = new List<Dude> {TheDude};

        var jsonSerializerSettings = new JsonSerializerSettings
        {
            PreserveReferencesHandling = PreserveReferencesHandling.Objects
        };

        Json = JsonConvert.SerializeObject(TheDude, jsonSerializerSettings);
    }

    [Test]
    public void Then_the_expected_json_is_created()
    {
        const string expected = @"{""$id"":""1"",""Bros"":[],""Name"":""The Dude"",""OldMan"":{""$id"":""2"",""Bros"":[],""Name"":""Gramps"",""OldMan"":null,""Sons"":[{""$ref"":""1""}]},""Sons"":[{""$id"":""3"",""Bros"":[{""$id"":""4"",""Bros"":[{""$ref"":""3""}],""Name"":""Lil' Bro"",""OldMan"":{""$ref"":""1""},""Sons"":[]}],""Name"":""Number one son"",""OldMan"":{""$ref"":""1""},""Sons"":[]},{""$ref"":""4""}]}";
        Assert.AreEqual(expected, Json);
    }

    [Test]
    public void Then_JsonConvert_can_recreate_the_original_graph()
    {
        // Providing a concrete type results in a complete graph
        var dude = JsonConvert.DeserializeObject<Dude>(Json);

        Assert.IsTrue(GraphEqualsOriginalGraph(dude));
    }

    [Test]
    public void Then_JsonConvert_can_recreate_the_original_graph_dynamically()
    {
        dynamic dude = JsonConvert.DeserializeObject(Json);

        // Calling ToObject with a concrete type results in a complete graph
        Assert.IsTrue(GraphEqualsOriginalGraph(dude.ToObject<Dude>()));
    }

    [Test]
    public void Then_JsonSerializer_can_recreate_the_original_graph()
    {
        var serializer = new JsonSerializer();
        serializer.Converters.Add(new ExpandoObjectConverter());
        var dude = serializer.Deserialize<ExpandoObject>(new JsonTextReader(new StringReader(Json)));

        // The graph is still dynamic, and as a result, the second occurrence of "The Dude" 
        // (as the son of "Gramps") will not be filled in completely.
        Assert.IsTrue(GraphEqualsOriginalGraph(dude));
    }

    private static bool GraphEqualsOriginalGraph(dynamic dude)
    {
        Assert.AreEqual("The Dude", dude.Name);
        Assert.AreEqual("Gramps", dude.OldMan.Name);
        Assert.AreEqual(2, dude.Sons.Count);
        Assert.AreEqual("Number one son", dude.Sons[0].Name);
        Assert.AreEqual("Lil' Bro", dude.Sons[0].Bros[0].Name);

        // The dynamic graph will not contain this object
        Assert.AreEqual("Lil' Bro", dude.Sons[1].Name);
        Assert.AreEqual("Number one son", dude.Sons[1].Bros[0].Name);
        Assert.AreEqual(1, dude.Sons[0].Bros.Count);
        Assert.AreSame(dude.Sons[0].Bros[0], dude.Sons[1]);
        Assert.AreEqual(1, dude.Sons[1].Bros.Count);
        Assert.AreSame(dude.Sons[1].Bros[0], dude.Sons[0]);

        // Even the dynamically graph forced through ToObject<Dude> will not contain this object.
        Assert.AreSame(dude, dude.OldMan.Sons[0]);

        return true;
    }
}
}

JSON:

{
   "$id":"1",
   "Bros":[

   ],
   "Name":"The Dude",
   "OldMan":{
      "$id":"2",
      "Bros":[

      ],
      "Name":"Gramps",
      "OldMan":null,
      "Sons":[
         {
            "$ref":"1"
         }
      ]
   },
   "Sons":[
      {
         "$id":"3",
         "Bros":[
            {
               "$id":"4",
               "Bros":[
                  {
                     "$ref":"3"
                  }
               ],
               "Name":"Lil' Bro",
               "OldMan":{
                  "$ref":"1"
               },
               "Sons":[

               ]
            }
         ],
         "Name":"Number one son",
         "OldMan":{
            "$ref":"1"
         },
         "Sons":[

         ]
      },
      {
         "$ref":"4"
      }
   ]
}

我已经看到很多在自定义 ValueProvider 中使用 Json.Net 的示例,以便完全支持这种情况,但没有一个解决方案对我有用。我认为缺少的关键是我见过的所有示例都没有处理反序列化为动态或扩展对象和具有内部引用的交集。

【问题讨论】:

  • 请发布您的 Json。 Json 没有引用。您所看到的引用只是可能对 some 序列化程序具有特殊含义的字符串。您可以使用 Json.NET 保留引用,如 Preserving Object References 所示。这会将$id$ref 属性添加到链接对象
  • Json 完全包含在我发布的示例中。甚至还有一个测试来确保生成的 Json 符合我的预期。它位于 Then_the_expected_json_is_created 方法中的“预期”变量中。该 Json 中有“$ref”,其中儿子们互相指代,而 Gramps 指的是“The Dude”。另外,这个例子是使用 Json.Net,而 Json.Net 实际上是问题标题中的第一件事。
  • Json 文本不可读,我认为它也在第二页或第三页。无论如何,您的链接表明您使用的语法与 Json.NET 相同。您只需在反序列化期间设置正确的设置
  • 如果有我没有尝试过的神奇设置组合,我很想看看。据我所知,这些设置在反序列化期间似乎并没有太大区别。但是,如果您希望 Json 在其中包含引用,则在序列化过程中肯定需要它们。我已经尝试了几乎所有我能找到的组合和设置,并且在反序列化方面似乎没有任何区别。
  • 请提供一个使用反序列化设置的最小示例。我只是表明您在发表评论后确实使用了这些设置,但这也是两页后的内容。在我开始写答案之后。我没有看到它们用于反序列化,所以我仍然会发布答案并回家(这里是晚上 8 点)

标签: .net json serialization json.net


【解决方案1】:

在与同事一刀两断后,上述行为对我来说是有意义的。

在不知道反序列化对象的类型的情况下,Json.Net 确实无法知道 Sons 或 Bros 属性不是意味着包含“{”$ref 的字符串属性":"1"}"...怎么可能呢? 当然它反序列化它是错误的。它必须知道目标类型,以便知道何时进一步反序列化对象的属性。

您最终会得到一个动态对象,其字符串属性包含对象引用的 Json 表示。当模型绑定器尝试使用这个动态对象来设置具体类型的值时,它找不到匹配项,最终得到一个空的目标实例。

Jason Butera 对this question 的回答最终成为最可行的解决方案。即使默认 ValueProvider 已经尝试(并且失败)将对象反序列化为字典以供 ModelBinder 使用,ModelBinder 也可以选择忽略所有这些,并将原始输入流从控制器上下文中拉出。由于 ModelBinder 确实 知道 Json 应该被反序列化为的类型,因此它可以将其提供给 JsonSerializer。也可以使用更方便的 JsonConvert.DeserializeObject 方法。

最终代码如下所示:

public class JsonNetModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var stream = controllerContext.RequestContext.HttpContext.Request.InputStream;
        stream.Seek(0, SeekOrigin.Begin);
        var streamReader = new StreamReader(stream, Encoding.UTF8);
        var json = streamReader.ReadToEnd();
        return JsonConvert.DeserializeObject(json, bindingContext.ModelType);
    }
}

Jason Butera 的回答使用一个属性来使用适当的 ModelBinder 标记每个控制器操作。我采取了更全球化的方法。在 Global.asax 中,我使用一点反射为我的所有 ViewModel 注册了自定义 ModelBinder:

var jsonModelBinder = new JsonNetModelBinder();
var viewModelTypes = typeof(ViewModelBase).Assembly.GetTypes()
    .Where(x => x.IsSubclassOf(typeof(ViewModelBase)));
viewModelTypes.ForEach(x => ModelBinders.Binders.Add(x, jsonModelBinder));

到目前为止,一切似乎都在工作,并且使用的代码比 ValueProvider 路由少得多。

【讨论】:

    【解决方案2】:

    Json 本身没有引用,这就是为什么 Json.Net 默认不尝试保留/恢复引用的原因。如果 Json.NET 每次找到 $id$ref 属性时都尝试恢复引用,那将是非常尴尬的。我还怀疑这会迫使解析器改变其解析策略,开始存储反序列化的对象和键等。

    您必须设置适当的反序列化设置,如Preserving Object References所示,例如:

    var settings=new JsonSerializerSettings {
                     PreserveReferencesHandling = PreserveReferencesHandling.Objects 
                 };
    var  deserializedPeople = JsonConvert.DeserializeObject<List<Person>>(json,settings);
    

    如果您仍然有问题,您应该尝试非常少的测试,即尝试使用小型 JSon sn-ps 并转向更复杂的测试。例如,文档示例是否有效?如果不是,您可能有一个旧的 Json.NET 版本。如果是,请尝试使用更复杂的示例,直到找到 Json.NET 的问题。

    如果每次都对文本 sn-p 进行小的更改,则比尝试调试整个序列化/反序列化链更容易找到问题

    【讨论】:

    • 这是最小的例子。这是我可以制作的最小的对象图,它具有循环父/子以及兄弟关系,并且证明了所述问题。至于文档示例?当然,它们完美无缺。它们还反序列化为具体类型,而不是动态类型,因此它们不适用于编写自定义 Json.Net ValueProvider 的上下文。这是一个非常具体的极端案例。动态反序列化带有循环引用的图。
    猜你喜欢
    • 2015-07-14
    • 2015-11-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-15
    • 1970-01-01
    • 2015-04-20
    相关资源
    最近更新 更多