Parsing Nested JSON Classes

I first encountered the problem of parsing nested JSON data objects when dealing with the Crunchbase API. Their JSON response looks something along the lines of:

{
  "metadata": {
    "version": 3,
    "www_path_prefix": "https://www.crunchbase.com/",
    "api_path_prefix": "https://api.crunchbase.com/v/3/",
    "image_path_prefix": "https://res.cloudinary.com/crunchbase-production/"
  },
  "data": {
    "paging": {
      "total_items": XXX,
      "number_of_pages": XXX,
      "current_page": XXX,
      "sort_order": "created_at DESC",
      "items_per_page": XXX,
      "next_page_url": "https://api.crunchbase.com/v/3/organizations?page=2&sort_order=created_at+DESC",
      "revision": XXX
    },
    "items": [
      {
        "type": "OrganizationSummary",
        "uuid": "XXX",
        "properties": {
          "permalink": "XXX",
          "api_path": "organizations/XXX",
          "web_path": "organization/XXX",
          "name": "XXX",
          "primary_role": "company",
          "short_description": "XXX",
          "profile_image_url": null,
          "domain": "XXX.com",
          "homepage_url": "http://www.XXX.com/",
          "facebook_url": null,
          "twitter_url": null,
          "linkedin_url": null,
          "city_name": null,
          "region_name": null,
          "country_code": null,
          "created_at": XXX,
          "updated_at": XXX
        }
      },
      ...
   ]
}

The Problem

In order to parse the above JSON (e.g. with JSON.NET), you would be expected to create a POCO object per nested level. If we simplified the above CrunchBase response to:

{
    "type": "Company",
    "guid": "66F7D7E1-0AB3-4A1F-8E47-EF935D899116",
    "properties": {
        "name": "Jason Poon Inc.",
        "description": "Builds Awesome Sh*t",
    }
}

we'd need to create two POCO classes:

public class Company  
{
    [JsonProperty("guid")]
    public Guid Guid { get; set; }

    [JsonProperty("type")]
    public string Type { get; set; }

    [JsonProperty("properties")]
    public CompanyProperties Properies { get; set; }
}

public class CompanyProperties  
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("description")]
    public string Description { get; set; }
}

You can very easily create these classes with JsonToCSharp, but considering the one-to-one mapping between Company and CompanyProperties, wouldn't it be great if we could denormalize all the data to a single POCO?

The Solution

Here's what a single POCO with the denormalized properties looks like:

public class Company: EntityBase  
{
    [JsonPropertyName("guid")]
    public Guid Guid { get; set; }

    [JsonPropertyName("type")]
    public string Type { get; set; }

    [JsonPropertyName("properties/name")]
    public string Name { get; set; }

    [JsonPropertyName("properties/description")]
    public string Description { get; set; }
}

<3 It looks so clean! There are two differences with the original:

  1. Each property is decorated with a JsonPropertyName attribute.
  2. The POCO inherits from a the base class EntityBase.

Custom Attribute

A custom attribute was created to hold the path of the JSON property.

public class JsonPropertyName : Attribute  
{
    public string Name { get; private set; }

    public JsonPropertyName(string name)
    {
        this.Name = name;
    }
}

Base Class

Here is where the magic happens. The base class exposes a Load method which does the hard work of parsing the raw JSON and setting the class properties given the path defined by the custom attribute.

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

public abstract class EntityBase  
{
    // this also a good spot to put other common properties
    [JsonPropertyName("properties/created_at")]
    public DateTime CreatedDateTime { get; set; }

    public virtual void Load(JToken json)
    {
        var propertyInfoArray = this.GetType().GetProperties();
        foreach (var prop in propertyInfoArray)
        {
            var attr = prop.GetCustomAttributes(true).Where(c => c.GetType() == typeof(JsonPropertyName)).Cast<JsonPropertyName>().FirstOrDefault();

            if (attr != null)
            {
                var jsonPropertyName = attr.Name;
                var keys = jsonPropertyName.Split('/');

                var value = json;
                foreach (var key in keys)
                {
                    value = value[key];
                    if (value == null)
                    {
                        break;
                    }
                }

                if (value != null && !String.IsNullOrEmpty(value.ToString()))
                {
                    prop.SetValue(this, Convert.ChangeType(value, prop.PropertyType));
                }
            }
        }
    }

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this); 
    }
}