Json.Net不动态反序列化引用



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

我找到了问题的根源。当我使用JsonConvert。使用具体的类型参数反序列化,一切正常,并且我得到了一个完整的对象图,其中子节点和兄弟节点可以正确地相互引用。但这对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"
      }
   ]
}

我见过很多使用Json的例子。Net中的自定义ValueProvider,以支持这个场景,没有一个解决方案对我有效。我认为缺少的关键是,我所见过的示例中没有一个处理反序列化为动态或扩展对象和具有内部引用的交集。

在与同事避过这个问题后,我明白了上面的行为。

不知道反序列化对象的类型,Json。Net真的没有办法知道儿子或兄弟的属性是不是意味着是一个字符串属性包含"{"$ref": "1"}"…怎么可能呢?当然它的反序列化是错误的。它知道目标类型,以便知道何时进一步反序列化对象的属性。

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

Jason Butera对这个问题的回答最终是最可行的解决方案。即使默认的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标记每个控制器动作。我采取了更全球化的方法。在全球。然后,我为我所有的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路由少得多。

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

您必须设置适当的反序列化设置,如保留对象引用中所示,例如:

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

如果你仍然有问题,你应该尝试非常小的测试,即尝试用小的JSon片段,然后转向更复杂的。例如,文档示例是否有效?如果没有,您可能有旧的Json。净版。如果是,尝试一个更复杂的例子,直到你发现是什么在困扰Json.NET。

如果每次都对文本片段进行少量更改,则比尝试调试整个序列化/反序列化链要容易得多

最新更新