06 February 2012

JSON deserialization with JSON.net: class hierarchies

In part 1 of this series I described the basics of creating classes from a JSON string and then simply deserializing the string into a (list of) classes. That way, you don’t have all the hooplah of SOAP, but still have strongly-typed classes in your client app. But beware, there is no formal contract either, so on a beautiful morning you might start to think that either you had too much of a drink yesterday evening, or that the company providing the data feed for your app indeed has started to sell Windows Phone 7 devices made by Sony, with a 65” screen.

Looking at the JSON string you now see something like this:

[
  {
    "Brand": "Nokia","Type" : "Lumia 800", "Device" : "Phone",
    "Specs":{"Storage" : "16GB", "Memory": "512MB","Screensize" : "3.7"}
  },
  {
    "Brand": "Sony", "Type" : "KDL-65HX920","Device" : "TV",
    "Specs":{"Screensize" : "65", "FullHD" : "Yes", "ThreeD" : "Yes" }
  },  
  { "Brand": "Nokia","Type" : "Lumia 900","Device" : "Phone",
    "Specs":{"Storage" : "8GB", "Memory": "512MB","Screensize" : "4.3" }
  },
  {
    "Brand": "Samsung", "Type" : "UE55C9000","Device" : "TV",
    "Specs":{"Screensize" : "55", "FullHD" : "Yes", "ThreeD" : "Yes" }
  },  
]
None of the two options mentioned before appear to be true: apparently the company has diversified. They are now selling TV's as well. Of course you could run this trough json2csharp, which will give you this:
public class Specs
{
    public string Storage { get; set; }
    public string Memory { get; set; }
    public string Screensize { get; set; }
    public string FullHD { get; set; }
    public string ThreeD { get; set; }
}

public class RootObject
{
    public string Brand { get; set; }
    public string Type { get; set; }
    public string Device { get; set; }
    public Specs Specs { get; set; }
}

This will work, but not for the purpose of what I’d like to show. We refactor the whole stuff into an object structure like this:

CodeScheme

Or, in code (put into a single file for the sake of brevity)

namespace JsonDemo
{
  public abstract class Device
  {
    public string Brand { get; set; }
    public string Type { get; set; }
  }
  
  public class Phone : Device
  {
    public PhoneSpecs Specs { get; set; }
  }
  
  public class Tv : Device
  {
    public TvSpecs Specs { get; set; }
  }
  
  public abstract class Specs
  {
    public string Screensize { get; set; }
  }
  
  public class PhoneSpecs : Specs
  {
    public string Storage { get; set; }
    public string Memory { get; set; }
  }
  
    public class TvSpecs: Specs
  {
    public string FullHd { get; set; }
    public string ThreeD { get; set; }
  }
}

If you think this is a ludicrous complicated way to store such a simple data structure I think you are quite right, but a) they don’t call me a Senior Software Architect for nothing, making things complicated is what I am told Architects do for a living, so I try to be a Good Boy ;-) and b) this is just for the purpose of the sample, so bear with me, right?

If you remember the crux of part 1: it all came down to one line of code, namely:

JsonConvert.DeserializeObject<List<Phone>>(r.EventArgs.Result);

This method actually has a second parameter: params Newtonsoft.Json.JsonConverter[] converters, which allows you to provide your own custom converters. Making those is pretty easy, and it becomes even more easy when you use the JsonCreationConverter<T> class that’s floating around the internet in various permutations. I nicked it from StackOverflow here.

using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace JsonDemo
{
  public abstract class JsonCreationConverter<T> : JsonConverter
  {
    protected abstract T Create(Type objectType, JObject jsonObject);

    public override bool CanConvert(Type objectType)
    {
      return typeof(T).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, 
      object existingValue, JsonSerializer serializer)
    {
      var jsonObject = JObject.Load(reader);
      var target = Create(objectType, jsonObject);
      serializer.Populate(jsonObject.CreateReader(), target);
      return target;
    }

    public override void WriteJson(JsonWriter writer, object value, 
   JsonSerializer serializer)
    {
      throw new NotImplementedException();
    }
  }
}

To make a converter, you subclass this object into a templated converter for the base class, override the “Create” method and off you go. Writing a converter for the Device/Phone/Tv hierarchy is pretty easy: you just have to check the value of the “Device” property, which you do like this:

using System;
using Newtonsoft.Json.Linq;

namespace JsonDemo
{
  public class JsonDeviceConverter : JsonCreationConverter<Device>
  {
    protected override Device Create(Type objectType, JObject jsonObject)
    {
      var typeName = jsonObject["Device"].ToString();
      switch(typeName)
      {
        case "TV":
          return new Tv();
        case "Phone":
          return new Phone();
        default: return null;
      }
    }
  }
}

For the Specs/PhoneSpecs/TvSpecs you do more or less the same, only now you have to check for the existence of certain properties, not the value. I decided that if an object has a “Storage” property it’s a PhoneSpecs, and if it has “FullHD” it’s a TVSpecs.

using System;
using Newtonsoft.Json.Linq;

namespace JsonDemo
{
  public class JsonSpecsConverter : JsonCreationConverter<Specs>
  {
    protected override Specs Create(Type objectType, JObject jsonObject)
    {
      if(jsonObject["Storage"] != null)
      {
        return new PhoneSpecs();
      }

      if (jsonObject["FullHD"] != null)
      {
        return new TvSpecs();
      }

      return null;
    }
  }
}

Finally, to get these converters being used by the deserializer you have to slightly modify the deserialization line:

var deserialized = JsonConvert.DeserializeObject<List<Device>>(r.EventArgs.Result, 
  new JsonDeviceConverter(), new JsonSpecsConverter());

And sure enough, if you put a breakpoint behind this line, you can place a watch on “deserialized” and see that the data structure has been deserialized in our artfully crafted class structure.

Deserialized

And that’s all there is to it. Demo solution with full code can be found here.

The third and final part of this series shows how to cache results and can be found here

3 comments:

Lck said...

Nice sample, thanks! :)

Mit Patel said...

You should set CanWrite to false in the generic implementation.

Also, you'll need the type names exposed (All) if you want to resolve the type in the converter by checking the JsonProperty "$type".

Joost van Schaik said...

@Mit your point being? Do you think the sample is incomplete? Would you mind to elaborate on that?