.net - Json.Net not deserializing references dynamically -
i have large object graph circular references, serializing json.net in order preserve references before sending client. on client-side, i'm using customized version of ken smith's jsonnetdecycle, is, in turn, based on douglas crockford's cycle.js restore circular object references on deserialization, , remove them again before sending objects server. on server side, i'm using custom jsondotnetvalueprovider similar 1 this question in order use json.net instead of stock mvc5 javascriptserializer. seems working fine server client , again, json surviving round-trip fine, mvc won't deserialize object graph correctly.
i've traced problem down this. when use jsonconvert.deserialize concrete type parameter, works, , complete object graph children , siblings referencing each other. won't work mvc valueprovider though, because don't know model type @ point in lifecycle. valueprovider supposed provide values in form of dictionary modelbinder use.
it appears me unless can provide concrete type deserialization, first reference given object in graph deserialize fine, subsequent references same object not. there's object there, has none of properties filled in.
to demonstrate, i've created smallest demonstration can of problem. in class (using json.net , nunit), create object graph, , attempt deserialize in 3 different ways. see additional comments inline.
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 1 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 1 son"",""oldman"":{""$ref"":""1""},""sons"":[]},{""$ref"":""4""}]}"; assert.areequal(expected, json); } [test] public void then_jsonconvert_can_recreate_the_original_graph() { // providing concrete type results in 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 concrete type results in 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))); // graph still dynamic, , result, second occurrence of "the dude" // (as son of "gramps") not 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 1 son", dude.sons[0].name); assert.areequal("lil' bro", dude.sons[0].bros[0].name); // dynamic graph not contain object assert.areequal("lil' bro", dude.sons[1].name); assert.areequal("number 1 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]); // dynamically graph forced through toobject<dude> not contain object. assert.aresame(dude, dude.oldman.sons[0]); return true; } } }
the 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 1 son", "oldman":{ "$ref":"1" }, "sons":[ ] }, { "$ref":"4" } ] }
i've seen plenty of examples of using json.net in custom valueprovider in order support scenario, , none of solutions have worked me. think key thing that's missing none of examples i've seen deal intersection of deserializing dynamic or expando object , having internal references.
after rubber ducking problem co-worker, above behavior makes sense me.
without knowing type of object it's deserializing, json.net has no way of knowing sons or bros properties aren't meant string properties containing "{"$ref": "1"}"... how it? of course deserializes wrong. has know target type in order know when further deserialize properties of object.
you end dynamic object string properties containing json representation of object references. when model binder tries use dynamic object set values on concrete type, finds no matches, , end empty instance of target.
jason butera's answer this question ends being viable solution. though default valueprovider has tried (and failed) deserialize object dictionary modelbinder use, modelbinder can choose ignore of that, , pull raw input stream off of controller context. since modelbinder does know type json supposed deserialized into, can provide jsonserializer. can make use of more convenient jsonconvert.deserializeobject method.
the final code looks this:
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's answer uses attribute mark each controller action proper modelbinder. took more global approach. in global.asax, register custom modelbinder of viewmodels using little reflection:
var jsonmodelbinder = new jsonnetmodelbinder(); var viewmodeltypes = typeof(viewmodelbase).assembly.gettypes() .where(x => x.issubclassof(typeof(viewmodelbase))); viewmodeltypes.foreach(x => modelbinders.binders.add(x, jsonmodelbinder));
it seems working far, , uses lot less code valueprovider route.
Comments
Post a Comment