Json.Net没有动态反序列化引用

时间:2016-09-07 15:34:13

标签: .net json serialization json.net

我有一个带有循环引用的大对象图,我在使用Json.Net进行序列化,以便在发送到客户端之前保留这些引用。在客户端,我使用的是Ken Smith的JsonNetDecycle的自定义版本,而后者又基于Douglas Crockford的cycle.js来恢复反序列化时循环对象引用,并在将对象发送回服务器之前再次删除它们。在服务器端,我使用类似于this问题的自定义JsonDotNetValueProvider,以便使用Json.Net而不是股票MVC5 JavaScriptSerializer。从服务器到客户端,所有东西似乎都运行良好,然后又回来了,Json幸免于往返,但是MVC不能正确地反序列化对象图。

我已将此问题追溯到此。当我使用JsonConvert.Deserialize和一个具体的类型参数时,一切正常,我得到一个完整的对象图,其中有孩子和兄弟姐妹正确引用彼此。但是,对于MVC ValueProvider来说,这不会起作用,因为您在生命周期的那一点上知道模型类型。 ValueProvider应该以字典的形式提供ModelBinder使用的值。

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

为了证明,我已经创造了我能解决的最小的演示。在这个类中(使用Json.Net和NUnit),我创建了一个对象图,并尝试以三种不同的方式对其进行反序列化。请参阅内联其他评论。

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的示例,以便完全支持这种情况,并且没有一个解决方案对我有用。我认为缺少的关键是我所见过的所有例子都没有涉及反序列化为动态或expando对象以及具有内部引用的交集。

2 个答案:

答案 0 :(得分: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路由少得多。

答案 1 :(得分:0)

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片段并转向更复杂的片段。例如,文档示例是否有效?如果不是,您可能有一个旧的Json.NET版本。如果是,请尝试使用更复杂的示例,直到找到错误的Json.NET。

如果每次对文本片段进行少量更改,那么找到问题要容易得多,而不是尝试调试整个序列化/反序列化链