I absolutely love Json.NET!
What I don't like is calling the non-generic DeserializeObject method and then having to deal with JToken wrappers. While these objects can be useful, I almost always want to just work directly with the data.
Good news, everyone! Newtonsoft natively supports deserializing to an ExpandoObject!
For anyone who does not know, ExpandoObjects are what .NET uses to let you create your own dynamic objects whose members can be dynamically added and removed at run time. The following two lines of code are ALL that you need to deserialize straight to an ExpandObjects:
var converter = new ExpandoObjectConverter();
dynamic obj = JsonConvert.DeserializeObject<ExpandoObject>(json, converter);
So why is this useful? To find out, let's take a look at some unit tests!
Unit Tests
// This is the sample class that we will use in our unit tests. It is
// simple, but shows examples of working with different data types.
public class Sample
{
public int[] A { get; set; }
public bool B { get; set; }
public Sample C { get; set; }
public static Sample Create()
{
return new Sample
{
A = new[] { 1, 2, 3 },
C = new Sample
{
A = new[] { 6, 5, 4 },
B = true
}
};
}
}
// This code is SO UGLY! Realistically you would never have to do this;
// you should cast to dynamic(like the test below). However I wanted to
// show what the JObjects are actually abstracting under the hood.
[Fact]
public void JObject()
{
var sample = Sample.Create();
var json = JsonConvert.SerializeObject(sample);
var obj = JsonConvert.DeserializeObject(json);
var jObj = obj as JObject;
Assert.NotNull(jObj);
var jTokenA = jObj["A"];
var jArray = jTokenA as JArray;
Assert.NotNull(jArray);
var jToken0 = jArray[0];
Assert.NotNull(jToken0);
var valueOf0 = jToken0.Value<long>();
Assert.Equal(sample.A[0], valueOf0);
var jTokenB = jObj["B"];
Assert.NotNull(jTokenB);
var valueOfB = jTokenB.Value<bool>();
Assert.Equal(sample.B, valueOfB);
var jTokenC = jObj["C"];
var jObjC = jTokenC as JObject;
Assert.NotNull(jObjC);
}
// Here is the correct way to write the test above. Note that we still
// have to call VALUE against the the tokens themselves. In our third
// test we will see how to avoid this by deserializing directly to
// ExpandoObject and skilling the JToken wrappers.
[Fact]
public void JObjectDynamic()
{
var sample = Sample.Create();
var json = JsonConvert.SerializeObject(sample);
dynamic obj = JsonConvert.DeserializeObject(json);
Assert.IsType<JObject>(obj);
var a = obj.A;
Assert.IsType<JArray>(a);
var a0 = a[0];
Assert.IsType<JValue>(a0);
Assert.Equal(sample.A[0], a0.Value); // Ewww!
var b = obj.B;
Assert.IsType<JValue>(b);
Assert.Equal(sample.B, b.Value); // Yuck!
var c = obj.C;
Assert.IsType<JObject>(c);
}
// Finally we arrive at the best way to write this. If you do not need
// the JToken wrappers around all of your values, then you can very
// easily deserialize stright into an ExpandoObject. This is the native
// type that .NET uses to handle dynamic objects. This means that we
// can now call our object exactly as we would if it were the real
// type; no need to check Value or any other wrapper properties. :)
[Fact]
public void ExpandoObject()
{
var sample = Sample.Create();
var json = JsonConvert.SerializeObject(sample);
var converter = new ExpandoObjectConverter();
dynamic obj = JsonConvert.DeserializeObject<ExpandoObject>(json, converter);
Assert.IsType<ExpandoObject>(obj);
var a = obj.A;
Assert.IsType<List<object>>(a);
var a0 = a[0];
Assert.IsType<long>(a0);
Assert.Equal(sample.A[0], a0); // No call to a0.Value!
var b = obj.B;
Assert.IsType<bool>(b);
Assert.Equal(sample.B, b); // Woo hoo!
var c = obj.C;
Assert.IsType<ExpandoObject>(c);
}
Enjoy,
Tom
What stops you doing this:
ReplyDeletedynamic obj = JsonConvert.DeserializeObject<dynamic>(json);
That will still deserialize to a JObject. If I change the ExpandoObject unit test to use that line, it will yield the following error:
DeleteXunit.Sdk.IsTypeException
Assert.IsType() Failure
Expected: System.Dynamic.ExpandoObject
Actual: Newtonsoft.Json.Linq.JObject
Just because we use ExpandoObjectConverter , mean that all objects inside the graph becomes ExpandoObjects itself, How do i test if that Object is instance of type Sample (especially the third property 'C' in the definition of Sample ), Then Assert.IsInstanceOfType(obj.c,typeof(sample)) work ??? Anyways that was a nice article
ReplyDeleteAs you pointed out, all objects inside of the graph will deserialize to ExpandObjects; thus you can not type check any of the sub properties with this method of deserialization. There are other strategies that you could use to accomplish this goal, however in my opinion those defeat the purpose of deserializing to dynamic types.
DeleteThis works great. One use case that this approach doesn't seem to support that JObject does is a json result that is a pure list at the root. Get an invalid cast exception from DeserializeObject. Any ideas on how make that use case work?
ReplyDeleteSystem.InvalidCastException: Unable to cast object of type 'System.Collections.Generic.List`1[System.Object]' to type 'System.Dynamic.ExpandoObject'
I posted a solution for this here http://stackoverflow.com/a/25081196/1477388
DeleteCan you explain how to map it back to an actual assembly type that will match the same signature? This requires every entity to have ICompareable .. is there another way to do this without having to write a JsonConverter and iterate over each property to fill for each entity type?
ReplyDeletevar t = Type.GetType(fullName);
if (t == null)
return null;
var newClass = (RegisterClientRequestDTO) obj; // Convert.ChangeType(obj, t);
This would only be if you didn't want to use a strong type. For your scenario I would recommend deserializing straight into your target type (RegisterClientRequestDTO).
DeleteHi Tom. Great article. Quick question: what's the purpose of "ExpandoObjectConverter" in the ExpandoObject test? The results are the same without or without the converter being passed to the "DeserializeObject" method.
ReplyDeleteUsing the ExpandoObjectConverter will cause DeserializeObject to return an ExpandoObject. By default DeserializeObject return will return a JObject.
DeleteNo, that's what the "ExpandoObject" type in "JsonConvert.DeserializeObject" is doing, not the converter. Removing the converter makes no difference in your tests.
DeleteIt seems you are right. I tried installing previous versions of Newtonsoft to see if this had changed, but it seems to work all the way back to v4.
DeleteGood catch, thanks for pointing it out!
So useful!
ReplyDelete