Click here to Skip to main content
15,867,594 members
Articles / Desktop Programming / WPF

Working with Newtonsoft.Json in C# & VB

Rate me:
Please Sign up or sign in to vote.
4.99/5 (96 votes)
31 Oct 2022CPOL12 min read 373.1K   15.5K   250   55
Working with simple JSON objects and collections to Custom Converters and Transformations into .NET classes - NewtonSoft.Json
In this article, we will see how to work with standard value types, custom value types, compress structures to types, and transform from the raw JSON data structure to class structures that support the needs of our applications.

Working with JSON Series

Downloads

Introduction

Whilst JSON is a compact and easy-to-read cross-language storage and data exchange format, the flexibility that it offers sometimes requires some custom handling to parse the data.

If you are not familiar with JSON, then here is a definition from the official http://www.json.org:

Quote:

JSON (JavaScript Object Notation) is a lightweight data-interchange format. It is easy for humans to read and write. It is easy for machines to parse and generate. It is based on a subset of the JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999.

Contents

Background

Why another article on JSON? This article was inspired by many questions asked in the CodeProject Quick Questions & Answers section of this website. We will use a number of basic to advanced real-life examples, from Etsy, Flickr, MovieDB, Google Drive, & Twitter, and solutions covering simple object serialization to custom converters and data transformations.

Tools & Libraries

Like anything, you need the right tools for the job. Here are some of the tools available including those used in this article.

Viewers & Validators

Image 1

Image 2

Sometimes, JSON data is packed and not very readable or we need to validate the raw data:

Code Generators

We need to create a class structure to convert the raw JSON data to. You could manually create classes from the JSON file which is a very slow and time-consuming task. There are far quicker ways to get this done. Here are a couple:

  • JSON Utils - supports both VB & C# with lots of options
  • Visual Studio Addin: Json2Csharp - converts JSON data on your clipboard to C# classes
  • quicktype.io - supports C#, TypeScript, Go Java, Elm, Swift, Simple Types, and Schemas

Serialization Libraries

Lastly, you need to map the raw JSON data to the class structure. Again, there are a few libraries out there:

For simple projects, all three libraries cover 99 ~ 100% of the requirements. For more intermediate and advanced work like custom data converters and transformations, we need Newtonsoft's Json.NET library. This will become more apparent later in this article.

Data Conversion

Once you have the raw JSON data and have created the classes to map the data to, the next step will be to deserialize to classes & serialize from classes. This article will focus on deserialization. The following helper class simplifies this task.

C#
using Newtonsoft.Json;
using System.Collections.Generic;

namespace Support.CSharp
{
    public static class JsonHelper
    {
        public static string FromClass<T>(T data, bool isEmptyToNull = false,
                                          JsonSerializerSettings jsonSettings = null)
        {
            string response = string.Empty;

            if (!EqualityComparer<T>.Default.Equals(data, default(T)))
                response = JsonConvert.SerializeObject(data, jsonSettings);

            return isEmptyToNull ? (response == "{}" ? "null" : response) : response;
        }

        public static T ToClass<T>
        (string data, JsonSerializerSettings jsonSettings = null)
        {
            var response = default(T);

            if (!string.IsNullOrEmpty(data))
                response = jsonSettings == null
                    ? JsonConvert.DeserializeObject<T>(data)
                    : JsonConvert.DeserializeObject<T>(data, jsonSettings);

            return response;
        }
    }
}
VB.NET
Imports Newtonsoft.Json

Public Module JsonHelper

    Public Function FromClass(Of T)(data As T,
                                    Optional isEmptyToNull As Boolean = False,
                                    Optional jsonSettings _
                                    As JsonSerializerSettings = Nothing) _
                                                          As String

        Dim response As String = String.Empty

        If Not EqualityComparer(Of T).Default.Equals(data, Nothing) Then
            response = JsonConvert.SerializeObject(data, jsonSettings)
        End If

        Return If(isEmptyToNull, (If(response = "{}", "null", response)), response)

    End Function

    Public Function ToClass(Of T)(data As String,
                                  Optional jsonSettings _
                                  As JsonSerializerSettings = Nothing) _
                                                        As T

        Dim response = Nothing

        If Not String.IsNullOrEmpty(data) Then
            response = If(jsonSettings Is Nothing,
                JsonConvert.DeserializeObject(Of T)(data),
                JsonConvert.DeserializeObject(Of T)(data, jsonSettings))
        End If

        Return response

    End Function

End Module

We won't be using the JsonSerializerSettings. You can read more about what these are in the Newtonsoft Json.NET documentation.

Standard Data Types

Let us start with something simple. The following two examples work with the .NET primitive data and collection types.

Simple Object Types

Here is a Category object from the Etsy API. All JSON fields map to .NET primitive data types.

JavaScript
{
        "category_id": 68890752,
        "name": "gloves",
        "meta_title": "Handmade Gloves on Etsy - Gloves, mittens, arm warmers",
        "meta_keywords": "handmade gloves, gloves, handmade arm warmers, 
         handmade fingerless gloves, handmade mittens, hand knit mittens, 
         hand knit gloves, handmade accessories",
        "meta_description": "Shop for unique, handmade gloves on Etsy, 
         a global handmade marketplace. Browse gloves, arm warmers, 
         fingerless gloves & more from independent artisans.",
        "page_description": "Shop for unique, handmade gloves from our artisan community",
        "page_title": "Handmade gloves",
        "category_name": "accessories\/gloves",
        "short_name": "Gloves",
        "long_name": "Accessories > Gloves",
        "num_children": 3
}

We need to generate class[es] to map the JSON data. To do this, we will use JSON Utils:

Image 3

Here, we have selected Pascal Case naming convention. The JSON fields are all in lowercase. Using the Json.NET JsonProperty attribute, mapping JSON fields to .NET class properties are quite simple. Here is the class generated:

C#
using Newtonsoft.Json;

namespace WinFormSimpleObject.Models
{
    public class Category
    {
        [JsonProperty("category_id")]
        public int CategoryId { get; set; }

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

        [JsonProperty("meta_title")]
        public string MetaTitle { get; set; }

        [JsonProperty("meta_keywords")]
        public string MetaKeywords { get; set; }

        [JsonProperty("meta_description")]
        public string MetaDescription { get; set; }

        [JsonProperty("page_description")]
        public string PageDescription { get; set; }

        [JsonProperty("page_title")]
        public string PageTitle { get; set; }

        [JsonProperty("category_name")]
        public string CategoryName { get; set; }

        [JsonProperty("short_name")]
        public string ShortName { get; set; }

        [JsonProperty("long_name")]
        public string LongName { get; set; }

        [JsonProperty("num_children")]
        public int NumChildren { get; set; }
    }
}
VB.NET
Imports Newtonsoft.Json

Namespace Models

    Public Class Category

        <JsonProperty("category_id")>
        Public Property CategoryId As Integer

        <JsonProperty("name")>
        Public Property Name As String

        <JsonProperty("meta_title")>
        Public Property MetaTitle As String

        <JsonProperty("meta_keywords")>
        Public Property MetaKeywords As String

        <JsonProperty("meta_description")>
        Public Property MetaDescription As String

        <JsonProperty("page_description")>
        Public Property PageDescription As String

        <JsonProperty("page_title")>
        Public Property PageTitle As String

        <JsonProperty("category_name")>
        Public Property CategoryName As String

        <JsonProperty("short_name")>
        Public Property ShortName As String

        <JsonProperty("long_name")>
        Public Property LongName As String

        <JsonProperty("num_children")>
        Public Property NumChildren As Integer

    End Class

End Namespace

Now we can deserialize the JSON data into .NET class[es]:

C#
public Category Result { get; set; }

private const string fileName = "Etsy_Category.Json";
private readonly string filePath = Environment.CurrentDirectory;

private void GetData()
{
    // Retrieve JSON data from file
    var rawJson = File.ReadAllText(Path.Combine(filePath, fileName));

    // Convert to C# Class typed object
    Result = JsonHelper.ToClass<Category>(rawJson);
}
VB.NET
Public Property Result() As Category

Private Const fileName As String = "Etsy_Category.Json"
Private ReadOnly filePath As String = Environment.CurrentDirectory

Private Sub GetData()

    ' Retrieve JSON data from file
    Dim rawJson = File.ReadAllText(Path.Combine(filePath, fileName))

    ' Convert to C# Class typed object
    Result = JsonHelper.ToClass(Of Category)(rawJson)

End Sub

And now can work with the data. Here is a screenshot of the attached sample app:

Image 4

Simple Collection Types

The Etsy API, like many other APIs, works with not only single objects but also collections of objects wrapped in a JSON response.

JavaScript
{
    "count": 27,
    "results": [{
        "category_id": 68890752,
        "name": "gloves",
        "meta_title": "Handmade Gloves on Etsy - Gloves, mittens, arm warmers",
        "meta_keywords": "handmade gloves, gloves, handmade arm warmers, 
         handmade fingerless gloves, handmade mittens, hand knit mittens, 
         hand knit gloves, handmade accessories",
        "meta_description": "Shop for unique, handmade gloves on Etsy, 
         a global handmade marketplace. Browse gloves, arm warmers, 
         fingerless gloves & more from independent artisans.",
        "page_description": "Shop for unique, handmade gloves from our artisan community",
        "page_title": "Handmade gloves",
        "category_name": "accessories\/gloves",
        "short_name": "Gloves",
        "long_name": "Accessories > Gloves",
        "num_children": 3
    },
    {
        "category_id": 68890784,
        "name": "mittens",
        "meta_title": "Handmade Mittens on Etsy - Mittens, gloves, arm warmers",
        "meta_keywords": "handmade mittens, handcrafted mittens, mittens, 
         accessories, gloves, arm warmers, fingerless gloves, mittens, 
         etsy, buy handmade, shopping",
        "meta_description": "Shop for unique, handmade mittens on Etsy, 
         a global handmade marketplace. Browse mittens, arm warmers, 
         fingerless gloves & more from independent artisans.",
        "page_description": "Shop for unique, 
         handmade mittens from our artisan community",
        "page_title": "Handmade mittens",
        "category_name": "accessories\/mittens",
        "short_name": "Mittens",
        "long_name": "Accessories > Mittens",
        "num_children": 4
    }],
    "params": {
        "tag": "accessories"
    },
    "type": "Category",
    "pagination": {
        
    }
}

Here is our response wrapper:

C#
public class Pagination
{
    [JsonProperty("effective_limit")]
    public int? EffectiveLimit { get; set; }

    [JsonProperty("effective_offset")]
    public int? EffectiveOffset { get; set; }

    [JsonProperty("effective_page")]
    public int? EffectivePage { get; set; }

    [JsonProperty("next_offset")]
    public int? NextOffset { get; set; }

    [JsonProperty("next_page")]
    public int? NextPage { get; set; }
}

public class Params
{
    [JsonProperty("tag")]
    public string Tag { get; set; }
}

public class Response<TModel> where TModel : class
{
    [JsonProperty("count")]
    public int Count { get; set; }

    [JsonProperty("results")]
    public IList<TModel> Results { get; set; }

    [JsonProperty("params")]
    public Params Params { get; set; }

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

    [JsonProperty("pagination")]
    public Pagination Pagination { get; set; }
}
VB.NET
Public Class Pagination

    <JsonProperty("effective_limit")>
    Public Property EffectiveLimit() As Integer?

    <JsonProperty("effective_offset")>
    Public Property EffectiveOffset() As Integer?

    <JsonProperty("effective_page")>
    Public Property EffectivePage() As Integer?

    <JsonProperty("next_offset")>
    Public Property NextOffset() As Integer?

    <JsonProperty("next_page")>
    Public Property NextPage() As Integer?

End Class

Public Class Params

    <JsonProperty("tag")>
    Public Property Tag As String

End Class

Public Class Response(Of TModel As Class)

    <JsonProperty("count")>
    Public Property Count As Integer

    <JsonProperty("results")>
    Public Property Results As IList(Of TModel)

    <JsonProperty("params")>
    Public Property Params As Params

    <JsonProperty("type")>
    Public Property Type As String

    <JsonProperty("pagination")>
    Public Property Pagination As Pagination

End Class

Now we can deserialize the JSON data into .NET class[es]:

C#
public BindingList<Category> Categories { get; set; }

private void GetData()
{
    // Retrieve JSON data from file
    var rawJson = File.ReadAllText(Path.Combine(filePath, fileName));

    // Convert to C# Class typed object
    var response = JsonHelper.ToClass<Response<Category>>(rawJson);

    // Get collection of objects
    if (response != null && response.Results != null && response.Results.Count > 0)
    {
        var data = response.Results;
        Categories.Clear();
        for (int i = 0; i < data.Count; i++)
        {
            Categories.Add(data[i]);
        }
    }
}
VB.NET
Public Property Categories() As BindingList(Of Category)

Private Sub GetData()

    ' Retrieve JSON data from file
    Dim rawJson = File.ReadAllText(Path.Combine(filePath, fileName))

    ' Convert to C# Class typed object
    Dim response = JsonHelper.ToClass(Of Response(Of Category))(rawJson)

    ' Get collection of objects
    If response IsNot Nothing _
        AndAlso response.Results IsNot Nothing _
        AndAlso response.Results.Count > 0 Then

        Dim data = response.Results
        Categories.Clear()
        For i As Integer = 0 To data.Count - 1
            Categories.Add(data(i))
        Next

    End If

End Sub

Now we can work with the data. Here is a screenshot of the attached sample app:

Image 5

Non-Standard Types and Data Structure Types

Not all languages across all platforms have compatible data types. Also, providers that support multiple data formats don't always have clean translations across data formats. The next section will cover these issues and address them with simple solutions.

UNIX Epoch Timestamps

What is a UNIX epoch timestamp? According to Wikipedia.org:

Quote:

A system for describing instants in time, defined as the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC), Thursday, 1 January 1970,[1][note 1] minus the number of leap seconds that have taken place since then.

Here is an example from Twitter:

JavaScript
"reset": 1502612374

And here is an example from Flickr:

JavaScript
"lastupdate": "1502528455"

We could have an integer property field and convert the integer epoch timestamp into a DateTime type post deserialization. The alternative and a better solution is to use a custom JsonConverter attribute:

C#
internal static class Unix
{
    internal static readonly DateTime Epoch = new DateTime
    (year: 1970, month: 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0, 
     kind: DateTimeKind.Utc);
}

public static class DoubleExtensions
{
    public static DateTime FromUnixDate(this double? unixDate)
    {
        return Unix.Epoch.AddSeconds(unixDate ?? 0.0);
    }

    public static DateTime FromUnixDate(this double unixDate)
    {
        return Unix.Epoch.AddSeconds(unixDate);
    }
}

public sealed class JsonUnixDateConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
    }

    public override object ReadJson(JsonReader reader, Type objectType, 
                    object existingValue, JsonSerializer serializer)
    {
        double value = 0;
        if (double.TryParse(Convert.ToString(reader.Value), out value))
            return value.FromUnixDate();

        if (objectType == typeof(DateTime))
            return default(DateTime);

        return null;
    }

    public override void WriteJson
    (JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
VB.NET
Friend Module Unix

    Friend ReadOnly Epoch As New DateTime(year:=1970, month:=1, day:=1, 
           hour:=0, minute:=0, second:=0, millisecond:=0, kind:=DateTimeKind.Utc)

End Module

Public Module DoubleExtensions

    <Extension>
    Public Function FromUnixDate(unixDate As Double?) As DateTime
        Return Epoch.AddSeconds(If(unixDate, 0.0))
    End Function

    <Extension>
    Public Function FromUnixDate(unixDate As Double) As DateTime
        Return Epoch.AddSeconds(unixDate)
    End Function

End Module

Public NotInheritable Class JsonUnixDateConverter : Inherits JsonConverter

    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return objectType Is GetType(DateTime) OrElse objectType Is GetType(Date?)
    End Function

    Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, 
           existingValue As Object, serializer As JsonSerializer) As Object

        Dim value As Double = 0
        If Double.TryParse(Convert.ToString(reader.Value), value) Then
            Return value.FromUnixDate()
        End If

        If objectType Is GetType(DateTime) Then
            Return Nothing
        End If

        Return Nothing

    End Function

    Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, 
                                   serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

End Class

How It Works

The JsonUnixDateConverter.CanConvert checks if it is the correct data type(s). If a match, the JsonUnixDateConverter.ReadJson executes, parses the value, converts from UNIX epoch to .NET Date type, and returns the value to be assigned to the class property.

How to Use

Simply apply the JsonUnixDateConverter to the property:

C#
[JsonProperty("lastupdate"), JsonConverter(typeof(JsonUnixDateConverter))]
public DateTime? LastUpdate { get; set; }
VB.NET
<JsonProperty("lastupdate"), JsonConverter(GetType(JsonUnixDateConverter))>
Public Property LastUpdate() As Date?

Data Structure Types

Some data providers support multiple data formats: XML, JSON, etc. and differences in the format can create some interesting data structure types. Data structure types are where a single variable type is described as an object rather than a simple value.

Image 6

Flickr has many examples of this where XML does not directly translate - XML has both attributes and elements describing data where JSON only has fields. An example of this is the Photo object and the comment count field:

JavaScript
{
    "photo": {
        "comments": {
            "_content": "483"
        }
    }
}

If we do a one-to-one translation, the classes required would be:

C#
public class Comments
{
    [JsonProperty("_content")]
    public int Count { get; set; }
}

public class Photo
{
    [JsonProperty("comments")]
    public Comments Comments { get; set; }
}
VB.NET
public class Comments
{
    [JsonProperty("_content")]
    public string Content { get; set; }
}

public class Photo
{
    [JsonProperty("comments")]
    public Comments Comments { get; set; }
}

Then to use the above class structure:

C#
int GetCommentCount(Photo photo)
{
    return photo.Comments.Count;
}
VB.NET
Private Function GetCommentCount(photo As Photo) As Integer
    Return photo.Comments.Count
End Function

It would be much better if we could simplify the Comment count into a single integer rather than a class object:

C#
int GetCommentCount(Photo photo)
{
    return photo.CommentCount;
}
VB.NET
Private Function GetCommentCount(photo As Photo) As Integer
    Return photo.CommentCount
End Function

Flickr has a number of other value data types that work the same. For example, the photo title field:

JavaScript
"title": {
    "_content": "North korean army Pyongyang North Korea \ubd81\ud55c"
}

The solution is a generic JsonConverter:

C#
public sealed class JsonFlickrContentConverter<TModel> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(TModel);
    }

    public override object ReadJson(JsonReader reader, Type objectType, 
                    object existingValue, JsonSerializer serializer)
    {
        var result = default(TModel);

        switch (reader.TokenType)
        {
            case JsonToken.Integer:
            case JsonToken.Float:
            case JsonToken.String:
            case JsonToken.Boolean:
            case JsonToken.Date:
                // convert from string or type to correct type
                // eg: "isfavorite": 0 to bool? = false
                result = (TModel)Convert.ChangeType(reader.Value, GetUnderlyingType());
                break;
            case JsonToken.StartObject:
                // sub-node _content holds actual value
                //  eg: "comments": { "_content": "483" } to int? = 483
                GetObject(reader, out result);
                break;
        }
        return result;
    }

    private void GetObject(JsonReader reader, out TModel result)
    {
        var tags = JObject.Load(reader).FindTokens("_content");
        result = (tags != null && tags.Count > 0)
            ? (TModel)Convert.ChangeType((string)tags[0], GetUnderlyingType())
            : result = default(TModel);
    }

    // converts Generic nullable type to underlying type
    // eg: int? to int
    private Type GetUnderlyingType()
    {
        var type = typeof(TModel);
        return Nullable.GetUnderlyingType(type) ?? type;
    }

    public override void WriteJson
    (JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
VB.NET
Public NotInheritable Class JsonFlickrContentConverter(Of TModel) : _
                                               Inherits JsonConverter

    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return objectType = GetType(TModel)
    End Function

    Public Overrides Function ReadJson(reader As JsonReader, _
    objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object

        Dim result = Nothing

        Select Case reader.TokenType
            Case JsonToken.[Integer],
                 JsonToken.Float,
                 JsonToken.[String],
                 JsonToken.[Boolean],
                 JsonToken.[Date]
                ' convert from string or type to correct type
                ' eg: "isfavorite": 0 to bool? = false
                result = DirectCast(Convert.ChangeType_
                         (reader.Value, GetUnderlyingType()), TModel)
                Exit Select
            Case JsonToken.StartObject
                ' sub-node _content holds actual value
                '  eg: "comments": { "_content": "483" } to int? = 483
                GetObject(reader, result)
                Exit Select
        End Select

        Return result

    End Function

    Private Sub GetObject(reader As JsonReader, ByRef result As TModel)

        Dim tags = JObject.Load(reader).FindTokens("_content")

        result = If((tags IsNot Nothing AndAlso tags.Count > 0),
            DirectCast(Convert.ChangeType(CType(tags(0), String), _
                       GetUnderlyingType()), TModel),
            Nothing)

    End Sub

    ' converts Generic nullable type to underlying type
    ' eg: int? to int
    Private Function GetUnderlyingType() As Type
        Dim type = GetType(TModel)
        Return If(Nullable.GetUnderlyingType(type), type)
    End Function

    Public Overrides Sub WriteJson(writer As JsonWriter, _
           value As Object, serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

End Class

And to use:

C#
public class Photo
{
    [JsonProperty("comments"), JsonConverter(typeof(JsonFlickrContentConverter<int?>))]
    public int? Comments { get; set; }
}
VB.NET
Public Class Photo
    <JsonProperty("comments"), _
     JsonConverter(GetType(JsonFlickrContentConverter(Of Integer?)))>
    Public Property Comments() As Integer?
End Class

Flattening Collection Types

Like data structure types, a collection of objects also sometimes do not translate well from XML to JSON. Flickr has many examples of this. The Photo.Notes collection is a typical example:

JavaScript
"notes": {
    "note": [{
        "id": "72157613689748940",
        "author": "22994517@N02",
        "authorname": "morningbroken",
        "authorrealname": "",
        "authorispro": 0,
        "x": "227",
        "y": "172",
        "w": "66",
        "h": "31",
        "_content": "Maybe ~ I think  ...She is very happy ."
    },
    {
        "id": "72157622673125344",
        "author": "40684115@N06",
        "authorname": "Suvcon",
        "authorrealname": "",
        "authorispro": 0,
        "x": "303",
        "y": "114",
        "w": "75",
        "h": "60",
        "_content": "this guy is different."
    }]
},

Class structure would be:

C#
public class Photo
{
    [JsonProperty("notes")]
    public Notes Notes { get; set; }
}

public class Notes
{
    [JsonProperty("note")]
    public IList<Note> Note { get; set; }
}
VB.NET
Public Class Photo

    <JsonProperty("notes")>
    Public Property Notes As Notes

End Class

Public Class Notes

    <JsonProperty("note")>
    Public Property Note As IList(Of Note)

End Class

As you can see, we end up with an extra unwanted Notes class to hold the list of Note.

The following generic JsonConverter will compress this to return a List<TModel> for the Photo class:

C#
public sealed class JsonFlickrCollectionConverter<TModel> : 
                    JsonConverter where TModel : class
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(TModel);
    }

    public override object ReadJson(JsonReader reader, _
    Type objectType, object existingValue, JsonSerializer serializer)
    {
        // A collection type containing only a collection of type
        // eg: "urls": { "url": [{...}, {...}, ...] }
        if (reader.TokenType == JsonToken.StartObject)
        {
            var jObj = JObject.Load(reader);
            if (jObj.HasValues)
            {
                var items = new List<TModel>();
                foreach (var value in jObj.Values())
                {
                    try
                    {
                        items.AddRange_
                        (JsonHelper.ToClass<IList<TModel>>(value.ToString()));
                    }
                    catch (Exception)
                    {
                        // unexpected type
                        return null;
                    }
                }
                return items;
            }
        }
        return null;
    }

    public override void WriteJson(JsonWriter writer, 
                    object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
VB.NET
Public NotInheritable Class JsonFlickrCollectionConverter(Of TModel As Class) : _
                      Inherits JsonConverter

    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return objectType = GetType(TModel)
    End Function

    Public Overrides Function ReadJson(reader As JsonReader, _
    objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object

        ' A collection type containing only a collection of type
        ' eg: "urls": { "url": [{...}, {...}, ...] }
        If reader.TokenType = JsonToken.StartObject Then

            Dim jObj = JObject.Load(reader)

            If jObj.HasValues Then

                Dim items = New List(Of TModel)

                For Each value In jObj.Values()
                    Try
                        items.AddRange(JsonHelper.ToClass_
                        (Of IList(Of TModel))(value.ToString()))
                    Catch generatedExceptionName As Exception
                        ' unexpected type
                        Return Nothing
                    End Try
                Next

                Return items

            End If

        End If

        Return Nothing

    End Function

    Public Overrides Sub WriteJson(writer As JsonWriter, _
           value As Object, serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

End Class

And to use:

C#
[JsonProperty("notes"), JsonConverter(typeof(JsonFlickrCollectionConverter<Note>))]
public IList<Note> Notes { get; set; }
VB.NET
<JsonProperty("notes"), JsonConverter(GetType(JsonFlickrCollectionConverter(Of Note)))>
Public Property Notes() As IList(Of Note)

Multi-Value Type Collections

This is where a collection of objects returned are of similar or different complex object types. themoviedb.org Multi-search returns TV, Move, and Person types in the same collection. Google Drive is another example that returns a collection with File and Folder complex object types.

MovieDB.Org

Here is a screenshot of the included MovieDB demo app that shows an example of this:

Image 7

The JSON data for the above screenshot looks like this:

JavaScript
{
    "page": 1,
    "total_results": 3433,
    "total_pages": 172,
  "results": [
    {
      "original_name": "Captain N and the New Super Mario World",
      "id": 26732,
      "media_type": "tv",
      "name": "Captain N and the New Super Mario World",
      "vote_count": 2,
      "vote_average": 3.5,
      "poster_path": "/i4Q8a0Ax5I0h6b1rHOcQEZNvJzG.jpg",
      "first_air_date": "1991-09-14",
      "popularity": 1.479857,
      "genre_ids": [
        16,
        35
      ],
      "original_language": "en",
      "backdrop_path": "/iYT5w3Osv3Bg1NUZdN9UYmVatPs.jpg",
      "overview": "Super Mario World is an American animated television series 
       loosely based on the Super NES video game of the same name. 
       It is the third and last Saturday morning cartoon based on the Super Mario Bros. 
       NES and Super NES series of video games. The show only aired 13 episodes 
       due to Captain N: The Game Master's cancellation on NBC. 
       Just like The Adventures of Super Mario Bros. 3, the series is produced by 
       DIC Entertainment and Reteitalia S.P.A in association with Nintendo, 
       who provided the characters and power-ups from the game.",
      "origin_country": [
        "US"
      ]
    },
    {
      "popularity": 1.52,
      "media_type": "person",
      "id": 1435599,
      "profile_path": null,
      "name": "Small World",
      "known_for": [
        {
          "vote_average": 8,
          "vote_count": 1,
          "id": 329083,
          "video": false,
          "media_type": "movie",
          "title": "One For The Road: Ronnie Lane Memorial Concert",
          "popularity": 1.062345,
          "poster_path": "/i8Ystwg81C3g9a5z3ppt3yO1vkS.jpg",
          "original_language": "en",
          "original_title": "One For The Road: Ronnie Lane Memorial Concert",
          "genre_ids": [
            10402
          ],
          "backdrop_path": "/oG9uoxtSuokJBgGO4XdC5m4uRGU.jpg",
          "adult": false,
          "overview": "At The Royal Albert Hall, London on 8th April 2004 
           after some 15 months of planning with Paul Weller, Ronnie Wood, 
           Pete Townshend, Steve Ellis, Midge Ure, Ocean Colour Scene amongst them 
           artists assembled to perform to a sell-out venue and to pay tribute to a man 
           who co-wrote many Mod anthems such as \"\"Itchycoo Park, All Or Nothing, 
           Here Comes The Nice, My Mind's Eye\"\" to name just a few. 
           Ronnie Lane was the creative heart of two of Rock n Rolls quintessentially 
           English groups, firstly during the 60's with The Small Faces then 
           during the 70;s with The Faces. After the split of the Faces he then 
           formed Slim Chance and toured the UK in a giant circus tent as well as 
           working in the studio with Eric Clapton, Pete Townshend and Ronnie Wood. 
           5,500 fans looked on in awe at The R.A.H as the superb evening's entertainment 
           ended with \"\"All Or Nothing\"\" featuring a surprise appearance by 
           Chris Farlowe on lead vocals.",
          "release_date": "2004-09-24"
        }
      ],
      "adult": false
    },
    {
      "vote_average": 6.8,
      "vote_count": 4429,
      "id": 76338,
      "video": false,
      "media_type": "movie",
      "title": "Thor: The Dark World",
      "popularity": 10.10431,
      "poster_path": "/bnX5PqAdQZRXSw3aX3DutDcdso5.jpg",
      "original_language": "en",
      "original_title": "Thor: The Dark World",
      "genre_ids": [
        28,
        12,
        14
      ],
      "backdrop_path": "/3FweBee0xZoY77uO1bhUOlQorNH.jpg",
      "adult": false,
      "overview": "Thor fights to restore order across the cosmos… 
       but an ancient race led by the vengeful Malekith returns to plunge 
       the universe back into darkness. Faced with an enemy that even Odin and Asgard 
       cannot withstand, Thor must embark on his most perilous and personal journey yet, 
       one that will reunite him with Jane Foster and force him to sacrifice everything 
       to save us all.",
      "release_date": "2013-10-29"
    }
  ]
}

The key to identifying the different complex data types is with a key field. In the above MovieDB JSON data example, the key field is media_type.

When we define the classes for the three data types, TV, Movie, and Person, we will use a simple Interface against each class type for the collection:

C#
public interface IDataType
{
}
VB.NET
Public Interface IDataType
End Interface

A custom JsonConverter JsonDataTypeConverter is used on the collection. It identifies the object type and generates the appropriate class and populates the fields:

C#
public sealed class JsonDataTypeConverter : JsonConverter
{
    // destination data type
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(IDataType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, 
                                    object existingValue, JsonSerializer serializer)
    {
        List<IDataType> results = null;

        // Are there multiple results?
        if (reader.TokenType== JsonToken.StartArray)
        {
            // Retrieve a list of Json objects
            var jObjs = serializer.Deserialize<IList<JObject>>(reader);
            if (jObjs != null && jObjs.Count > 0)
            {
                results = new List<IDataType>();

                for (int i = 0; i < jObjs.Count; i++)
                {
                    // Does the object contain a "media_type" 
                    // identifier - eg: "media_type":
                    var token = jObjs[i].FindTokens("media_type");
                    if (token != null && token.Count > 0)
                    {
                        // Only one is expected
                        switch (token[0].ToString())
                        {
                            // "media_type": "tv"
                            case "tv":
                                results.Add(Convert<TV>(jObjs[i]));
                                break;
                            // "media_type": "movie"
                            case "movie":
                                results.Add(Convert<Movie>(jObjs[i]));
                                break;
                            // "media_type": "person"
                            case "person":
                                results.Add(Convert<Person>(jObjs[i]));
                                break;
                        }
                    }
                }
            }
        }
        return results;
    }

    // Convert Json Object data into a specified class type
    private TModel Convert<TModel>(JObject jObj) where TModel : IDataType
    {
        return JsonHelper.ToClass<TModel>(jObj.ToString());
    }

    // one way conversion, so back to Json is not required
    public override void WriteJson
    (JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
VB.NET
Public NotInheritable Class JsonDataTypeConverter : Inherits JsonConverter

    ' destination data type
    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return objectType = GetType(IDataType)
    End Function

    Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, _
                     existingValue As Object, serializer As JsonSerializer) As Object

        Dim results As List(Of IDataType) = Nothing

        ' Are there multiple results?
        If reader.TokenType = JsonToken.StartArray Then

            ' Retrieve a list of Json objects
            Dim jObjs = serializer.Deserialize(Of IList(Of JObject))(reader)
            If jObjs IsNot Nothing AndAlso jObjs.Count > 0 Then
                results = New List(Of IDataType)()

                For i As Integer = 0 To jObjs.Count - 1

                    ' Does the object contain a "media_type" identifier - 
                    ' e.g.: "media_type":
                    Dim token = jObjs(i).FindTokens("media_type")
                    If token IsNot Nothing AndAlso token.Count > 0 Then

                        ' Only one is expected
                        Select Case token(0).ToString()
                            ' "media_type": "tv"
                            Case "tv"
                                results.Add(Convert(Of TV)(jObjs(i)))
                                Exit Select
                            ' "media_type": "movie"
                            Case "movie"
                                results.Add(Convert(Of Movie)(jObjs(i)))
                                Exit Select
                            ' "media_type": "person"
                            Case "person"
                                results.Add(Convert(Of Person)(jObjs(i)))
                                Exit Select
                        End Select

                    End If
                Next
            End If
        End If

        Return results

    End Function

    ' Convert Json Object data into a specified class type
    Private Function Convert(Of TModel As IDataType)(jObj As JObject) As TModel
        Return JsonHelper.ToClass(Of TModel)(jObj.ToString())
    End Function

    ' one way conversion, so back to Json is not required
    Public Overrides Sub WriteJson(writer As JsonWriter, _
                                   value As Object, serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

End Class

And to use:

C#
public class Response
{
    [JsonProperty("results"), JsonConverter(typeof(JsonDataTypeConverter))]
    public IList<IDataType> Results { get; set; }
}
VB.NET
Public Class Response

    <JsonProperty("results"), JsonConverter(GetType(JsonDataTypeConverter))>
    Public Property Results() As IList(Of IDataType)

End Class

Below is a screenshot of the collection when viewing the IntelliSense debug window showing the collection with multiple object types:

Image 8

Google Drive

Another example of this is Google Drive API. The File type, for example, has two identifiers: one to differentiate between Files and Folder, and another for the type of File - Jpg, Png, Text file, Document, MP4, etc. This can be seen in the JSON data.

JavaScript
{
 "kind": "drive#fileList",
 "incompleteSearch": false,
 "files": [
  {
   "kind": "drive#file",
   "mimeType": "video/mp4"
  },
  {
   "kind": "drive#file",
   "mimeType": "application/vnd.google-apps.folder"
  },
  {
   "kind": "drive#file",
   "mimeType": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
  },
  {
   "kind": "drive#file",
   "mimeType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
  },
  {
   "kind": "drive#file",
   "mimeType": "text/plain"
  },
  {
   "kind": "drive#file",
   "mimeType": "image/png"
  }
 ]
}

The different File types can be seen reflected in the following sample application screenshot:

Image 9

The sample app above works with a Hierarchical data structure and loads each branch's data on-demand as it is opened.

All the different types of files have the same data. So we can declare a base type and inherit it for all the different types of files.

C#
public class File : IResourceKind
{
    [JsonProperty("kind")]
    public string Kind { get; set; }

    [JsonProperty("mimeType")]
    public string MimeType { get; set; }
}

public class Folder : File
{
}

public class TxtDocument : File
{
}

public class WordDocument : File
{
}

public class PngImage : File
{
}

public class JpgImage : File
{
}

public class Zipped : File
{
}
VB.NET
Public Class File : Implements IResourceKind

    <JsonProperty("kind")>
    Public Property Kind As String

    <JsonProperty("mimeType")>
    Public Property MimeType As String

End Class

Public Class Folder : Inherits File
End Class

Public Class TxtDocument : Inherits File
End Class

Public Class WordDocument : Inherits File
End Class

Public Class PngImage : Inherits File
End Class

Side Note: When working with Implicit templates in XAML, you can define a default template, in this case for the base File Type, and if an implicit template is not found for the data type (class), the base template is applied for the inherited base class type.

XML
<DataTemplate DataType="{x:Type m:File}">
    <!-- Template here -->
</DataTemplate>

As there is a common base type for a large number of File types, a compact handler inside a customer JsonConverter, JsonDataTypeConverter for Google Drive, can be used. Here, we will have a lookup Dictionary Table for the "mime_type" JSON field and method reference with conversion type.

C#
// only some types are lists for brevity
private readonly Dictionary<string, Func<JObject, File>> mimeTypes
    = new Dictionary<string, Func<JObject, File>>
{
    { "application/vnd.google-apps.folder", Convert<Folder>() },
    { "image/jpeg", Convert<JpgImage>() },
    { "image/png", Convert<PngImage>() },
    { "application/zip", Convert<Zipped>() },
    { "application/x-zip-compressed", Convert<Zipped>() },
    { "video/mp4", Convert<Mp4Video>() },
    { "text/plain", Convert<TxtDocument>() },
    { "application/vnd.openxmlformats-officedocument.presentationml.presentation",
      Convert<PptDocument>() },
    { "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      Convert<WordDocument>() }
};

// Convert file of type.... mimeTypes
private void ProcessFileType(List<File> results, IList<JObject> jObjs, int i)
{
    var fileToken = jObjs[i].FindTokens("mimeType");
    if (fileToken != null && fileToken.Count > 0)
    {
        var key = mimeTypes.Keys.FirstOrDefault(x => x.Equals(fileToken[0].ToString()));
        if (key != null)
        {
            results.Add(mimeTypes[key](jObjs[i]));
        }
    }
}

// Convert Json Object data into a specified class type
private static Func<JObject, File> Convert<TModel>() where TModel : File
{
    return (jObj) => JsonHelper.ToClass<TModel>(jObj.ToString());
}
VB.NET
' only some types are lists for brevity
Private ReadOnly mimeTypes As New Dictionary(Of String, Func(Of JObject, File))() From {
    {"application/vnd.google-apps.folder", Convert(Of Folder)()},
    {"image/jpeg", Convert(Of JpgImage)()},
    {"image/png", Convert(Of PngImage)()},
    {"application/zip", Convert(Of Zipped)()},
    {"application/x-zip-compressed", Convert(Of Zipped)()},
    {"video/mp4", Convert(Of Mp4Video)()},
    {"text/plain", Convert(Of TxtDocument)()},
    {"application/vnd.openxmlformats-officedocument.presentationml.presentation",
     Convert(Of PptDocument)()},
    {"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
     Convert(Of WordDocument)()}
}

' Convert file of type.... mimeTypes
Private Sub ProcessFileType(results As List(Of File), _
                            jObjs As IList(Of JObject), i As Integer)

    Dim fileToken = jObjs(i).FindTokens("mimeType")

    If fileToken IsNot Nothing AndAlso fileToken.Count > 0 Then

        Dim key = mimeTypes.Keys.FirstOrDefault(Function(x) _
                  x.Equals(fileToken(0).ToString()))

        If key IsNot Nothing Then
            results.Add(mimeTypes(key)(jObjs(i)))
        End If

    End If

End Sub

' Convert Json Object data into a specified class type
Private Shared Function Convert(Of TModel As File)() As Func(Of JObject, File)
    Return Function(jObj) JsonHelper.ToClass(Of TModel)(jObj.ToString())
End Function

The IntelliSense debug window for the File collection property correctly reflects the recursive deserialization of the different File types:

Image 10

Recursive Deserialization

JSON object structures can be many node levels deep. Each node could have properties with their own custom JsonConverters. An example of this is the MovieDB above in the previous section Multi-Value Type Collections. Here is an extract for the Person JSON node structure with the node collection known_for :

JavaScript
{
    "page": 1,
    "total_results": 3433,
    "total_pages": 172,
  "results": [
    {
      "media_type": "tv",
    },
    {
      "media_type": "person",
      "known_for": [
        {
          "media_type": "movie",
        }
      ]
    },
    {
      "media_type": "movie",
    }
  ]
}

The JsonDataTypeConverter class above already supports recursive deserialization. Once the node object type is identified, we recall the JsonHelper.ToClass<...>(...) method:

C#
// Convert Json Object data into a specified class type
private TModel Convert<TModel>(JObject jObj) where TModel : IDataType
{
    return JsonHelper.ToClass<TModel>(jObj.ToString());
}
VB.NET
' Convert Json Object data into a specified class type
Private Function Convert(Of TModel As IDataType)(jObj As JObject) As TModel
    Return JsonHelper.ToClass(Of TModel)(jObj.ToString())
End Function

And in the Person class, we apply the JsonConverter attribute JsonDataTypeConverter:

C#
public class Person : RecordBase
{
    [JsonProperty("known_for"), JsonConverter(typeof(JsonDataTypeConverter))]
    public IList<IDataType> KnownFor { get; set; }
}
VB.NET
Public Class Person : Inherits RecordBase

    <JsonProperty("known_for"), JsonConverter(GetType(JsonDataTypeConverter))>
    Public Property KnownFor As IList(Of IDataType)

End Class

Here, we can see from the IntelliSense debug window for the Person.KnownFor collection property and correctly reflects the recursive deserialization of the Movie class type:

Image 11

Handling Invalid Collection Property Names in a JavaScript JSON File

JavaScript is not as strict as the C# language and allows property names that are invalid for C#. For example (an extract from the output of Json generated by JavaScript Navigator object):

{
    ...
    "plugins": {
        "0": {
            "0": {},
            "1": {}
        },
        "1": {
            "0": {},
            "1": {}
        },
        "2": {
            "0": {},
            "1": {}
        },
        "3": {
            "0": {},
            "1": {}
        },
        "4": {
            "0": {},
            "1": {}
        }
    },
    ...
}

As you can see, we have invalid property names like "0", "1", "2", etc. In C#, property names must start with an alphanumeric character. As this is a collection, we can not just use a JsonProperty attribute on a number of properties as we do not know how many elements/properties to expect.

If you tried to deserialize the above JSON "plugins" object without a custom JsonConverter, you would see something like the following error:

Image 12

A standard JSON collection is enclosed within square brackets and looks like this:

{
    ....
    "languages": [
        "en-GB",
        "en",
        "en-US"
    ],
    ....
}

There are a number of ways to handle the "plugins" collection in a custom JsonConverter. You could choose to use Dictionary<string, <Dictionary<string, Object>> or List<Dictionary<string, Object>> or List<List<Object>>. For this article, I have chosen to embrace non-standard property naming by using classes.

C#
public class UserAgentModel
{
    // trimmed for brevity

    [JsonProperty("plugins")]
    [JsonConverter(typeof(UserAgentPluginsConverter))]
    public List<PluginModel> Plugins { get; set; }
}

public class PluginModel
{
    public string Id { get; set; }
    public List<PluginObjectModel> Values { get; set; }
}

public class PluginObjectModel
{
    public string Id { get; set; }
    public object Value { get; set; }
}
VB.NET
Public Class UserAgentModel

    ' trimmed for brevity

    <JsonProperty("plugins")>
    <JsonConverter(GetType(UserAgentPluginsConverter))>
    Public Property Plugins As List(Of PluginModel)

End Class

Public Class PluginModel

    Public Property Id As String
    Public Property Values As List(Of PluginObjectModel)

End Class

Public Class PluginObjectModel

    Public Property Id As String
    Public Property Value As Object

End Class

A custom JsonConverter UserAgentPluginsConverter is used to map the JSON to the class structure.

C#
public class UserAgentPluginsConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(PluginModel);
    }
 
    public override object ReadJson(JsonReader reader, Type objectType,
                                    object existingValue, JsonSerializer serializer)
    {
        List<PluginModel> result = new List<PluginModel>();
 
        // Is this the start of an object?
        if (reader.TokenType == JsonToken.StartObject)
        {
            var jObj = JObject.Load(reader);
            if (jObj.HasValues)
            {
                // Step through each top property node
                foreach (var child in jObj.Children())
                {
                    result.Add(new PluginModel
                    {
                        Id = child.Path,
                        Values =  ProcessChild(child, serializer)
                    });
                } 
            }
        }
        
        return result;
    }
 
    private List<PluginObjectModel> ProcessChild(JToken child, JsonSerializer serializer)
    {
        var objects = new List<PluginObjectModel>();
        var node = serializer.Deserialize<JProperty>(child.CreateReader());
 
        if (node.HasValues)
        {
            foreach (var jToken in node.Values())
            {
                var item = serializer.Deserialize<JProperty>(jToken.CreateReader());
                objects.Add(new PluginObjectModel
                {
                    Id = item.Name,
                    Value = item.Value.ToString()
                });
            } 
        }
 
        return objects;
    }
 
    public override void WriteJson
    (JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
VB.NET
Public Class UserAgentPluginsConverter : Inherits JsonConverter

    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return objectType = GetType(PluginModel)
    End Function

    Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, _
                                       existingValue As Object, _
                                       serializer As JsonSerializer) As Object

        Dim result = New List(Of PluginModel)()

        If reader.TokenType = JsonToken.StartObject Then
            Dim jObj = JObject.Load(reader)

            If jObj.HasValues Then

                For Each child In jObj.Children()
                    result.Add(New PluginModel With {
                        .Id = child.Path,
                        .Values = ProcessChild(child, serializer)
                    })
                Next
            End If
        End If

        Return result
    End Function

    Private Function ProcessChild(child As JToken, serializer As JsonSerializer) _
                                  As List(Of PluginObjectModel)

        Dim objects = New List(Of PluginObjectModel)()
        Dim node = serializer.Deserialize(Of JProperty)(child.CreateReader())

        If node.HasValues Then

            objects.AddRange(From jToken In node.Values()
                             Select item = serializer.Deserialize_
                             (Of JProperty)(jToken.CreateReader())
                             Select New PluginObjectModel With
                                {
                                    .Id = item.Name,
                                    .Value = item.Value.ToString()
                                })
        End If

        Return objects
    End Function

    Public Overrides Sub WriteJson(writer As JsonWriter, _
                     value As Object, serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

End Class

Now our model will look as we expect.

Image 13

And finally, our sample application can display the data.

Image 14

Data Transformation

Normally, when transforming data, it is a two-step process. First, we convert the JSON data to .NET classes (34 classes in total in this case), then transform the data.

Image 15

The last example that I have demonstrated how to transform the JSON data to a custom class collection in a single step. By doing this, we have reduced the number of classes required, in this case, from 34 to 4!

Image 16

Here is what the end result will look like:

Image 17

As there are too many data classes to post here, I will only discuss the JsonConverter used. You can view and run the sample project,WpfApplicationRateLimitStatus which is included in the download. I have included both the standard and custom mapping classes.

The JsonApiRateLimitsConverter compresses the multiple classes into a simpler collection of data that is compatible with the application's requirements.

C#
public sealed class JsonApiRateLimitsConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(RateCategoryModel);
    }

    public override object ReadJson(JsonReader reader, Type objectType, 
                    object existingValue, JsonSerializer serializer)
    {
        ObservableCollection<RateCategoryModel> result = null;

        // is this the start of an object?
        if (reader.TokenType == JsonToken.StartObject)
        {
            var jObj = JObject.Load(reader);
            if (jObj.HasValues)
            {
                // Step through each top property node
                result = new ObservableCollection<RateCategoryModel>();
                foreach (var child in jObj.Children())
                {
                    var rate = ProcessChild(child, serializer);
                    if (rate != null) result.Add(rate);
                }
            }
        }
        return result;
    }

    private static RateCategoryModel ProcessChild(JToken child, JsonSerializer serializer)
    {
        RateCategoryModel rate = null;
        var node = serializer.Deserialize<JProperty>(child.CreateReader());
        if (node.HasValues)
        {
            // step through each sub-property node
            rate = new RateCategoryModel { Name = node.Name };
            foreach (var item in node.Values())
            {
                var limit = serializer.Deserialize<JProperty>(item.CreateReader());
                rate.Limits.Add(new RateLimitModel
                {
                    // we only want the last part of the property name
                    Name = GetPropertyName(limit),
                    Limit = GetRateLimit(limit)
                });
            }
        }
        return rate;
    }

    private static string GetPropertyName(JProperty prop)
    {
        string name = string.Empty;

        if (!string.IsNullOrEmpty(prop.Name))
        {
            var parts = prop.Name.Split(new[] { '/' }, 
                        StringSplitOptions.RemoveEmptyEntries);
            name = string.Join("_", parts.Skip(1)).Replace(":", "");
            if (string.IsNullOrEmpty(name))
                name = prop.Name.Replace("/", "");
        }
        else
        {
            name = "__noname__";
        }

        return name;
    }

    private static ApiRateLimitModel GetRateLimit(JProperty limit)
    {
        ApiRateLimitModel model = null;

        if (!string.IsNullOrEmpty(limit.First.ToString()))
        {
            var rawJson = limit.First.ToString();
            model = JsonHelper.ToClass<ApiRateLimitModel>(rawJson);
        }

        return model;
    }

    public override void WriteJson
    (JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
VB.NET
Public NotInheritable Class JsonApiRateLimitsConverter : Inherits JsonConverter

    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return objectType = GetType(RateCategoryModel)
    End Function

    Public Overrides Function ReadJson(reader As JsonReader, _
      objectType As Type, existingValue As Object, 
                          serializer As JsonSerializer) As Object

        Dim result As ObservableCollection(Of RateCategoryModel) = Nothing

        ' is this the start of an object?
        If reader.TokenType = JsonToken.StartObject Then

            Dim jObj = JObject.Load(reader)
            If jObj.HasValues Then

                ' Step through each top property node
                result = New ObservableCollection(Of RateCategoryModel)()
                For Each child In jObj.Children()

                    Dim rate = ProcessChild(child, serializer)

                    If rate IsNot Nothing Then
                        result.Add(rate)
                    End If

                Next

            End If
        End If

        Return result

    End Function

    Private Shared Function ProcessChild(child As JToken, _
            serializer As JsonSerializer) As RateCategoryModel

        Dim rate As RateCategoryModel = Nothing
        Dim node = serializer.Deserialize(Of JProperty)(child.CreateReader())

        If node.HasValues Then

            ' step through each sub-property node
            rate = New RateCategoryModel() With {.Name = node.Name}

            For Each item In node.Values()

                Dim limit = serializer.Deserialize(Of JProperty)(item.CreateReader())

                ' we only want the last part of the property name
                rate.Limits.Add(New RateLimitModel() With
                {
                    .Name = GetPropertyName(limit),
                    .Limit = GetRateLimit(limit)
                })

            Next

        End If

        Return rate

    End Function

    Private Shared Function GetPropertyName(prop As JProperty) As String

        Dim name As String = String.Empty

        If Not String.IsNullOrEmpty(prop.Name) Then

            Dim parts = prop.Name.Split(New Char() _
                        {"/"c}, StringSplitOptions.RemoveEmptyEntries)
            name = String.Join("_", parts.Skip(1)).Replace(":", "")

            If String.IsNullOrEmpty(name) Then
                name = prop.Name.Replace("/", "")
            End If

        Else
            name = "__noname__"
        End If

        Return name

    End Function

    Private Shared Function GetRateLimit(limit As JProperty) As ApiRateLimitModel

        Dim model As ApiRateLimitModel = Nothing

        If Not String.IsNullOrEmpty(limit.First.ToString()) Then
            Dim rawJson = limit.First.ToString()
            model = JsonHelper.ToClass(Of ApiRateLimitModel)(rawJson)
        End If

        Return model

    End Function

    Public Overrides Sub WriteJson(writer As JsonWriter, _
           value As Object, serializer As JsonSerializer)
        Throw New NotImplementedException()
    End Sub

And to use:

C#
public class APIRateStatusModel
{
    [JsonProperty("resources"), JsonConverter(typeof(JsonApiRateLimitsConverter))]
    public ObservableCollection<RateCategoryModel> Resources { get; set; }
}
VB.NET
Public Class APIRateStatusModel

    <JsonProperty("resources"), JsonConverter(GetType(JsonApiRateLimitsConverter))>
    Public Property Resources() As ObservableCollection(Of RateCategoryModel)

End Class
C#
private void GetData()
{
    // Retrieve JSON data from file
    var rawJson = File.ReadAllText(Path.Combine(filePath, fileName));

    // Convert to C# Class List of typed objects
    Result = JsonHelper.ToClass<APIRateStatusModel>(rawJson);
}
VB.NET
Private Sub GetData()

    ' Retrieve JSON data from file
    Dim rawJson = File.ReadAllText(Path.Combine(filePath, fileName))

    ' Convert to C# Class List of typed objects
    Result = JsonHelper.ToClass(Of APIRateStatusModel)(rawJson)

End Sub

Summary

We have covered how to work with standard value types, custom value types, compress structures to types, and transform from the raw JSON data structure to class structures that support the needs of our applications. Newtonsoft's Json.NET, the Helper classes, and the custom JsonConverters make working with JSON data clean and very easy.

Sample Applications

There are seven (7) sample applications, both in C# and VB, included in the download:

  1. WinFormSimpleObject - Etsy Category
    • WinForm, code-behind
  2. WinFormSimpleCollection - Etsy Categories
    • WinForm, Datbinding, MVVM (simple)
  3. WpfPhoto - Flickr Photo Viewer
    • WPF, MVVM
    • JsonFlickrCollectionConverter, JsonFlickrContentConverter, JsonFlickrUnixDateContentConverter, JsonFlickrUriContentConverter, StringEnumConverter
  4. WpfMultiSearch - MovieDB MultiSearch result
    • WPF, MVVM, Implicit Templates
    • JsonDataTypeConverter, JsonPartialUrlConverter
  5. WpfApplicationRateLimitStatus - Twitter JSON data Transformation
    • WPF, MVVM, DataGrid
    • JsonUnixDateConverter, JsonApiRateLimitsConverter
  6. WpfFileExplorer - Google Drive File Explorer
    • WPF, MVVM, TreeView with load-on-demand & custom template, Implicit Templates, Hierarchical DataBinding
    • JsonDataTypeConverter, JsonGoogleUriContentConverter
  7. WpfUserAgent - Handling invalid collection property names in a Javascript JSON file
    • WPF, MVVM, TreeView with Implicit Templates, Hierarchical DataBinding
    • UserAgentPluginsConverter

A download link for all the samples is provided above.

History

  • v1.0 - August 15, 2017 - Initial release
  • v1.1 - August 16, 2017 - Added reference to Fiddler in the Viewers & Validators section
  • v1.2 - August 16, 2017 - Added new Recursive Deserialization section (thanks BillWoodruff for the suggestion); new download with a minor update to C#/VB WpfMultiSearch sample
  • v1.3 - August 19, 2017 - Added new Google Drive File Explorer sample app to demonstrate another version of Multi-Value Type Collections with multiple identifier keys for file type class support broken down in the Multi-Value Type Collections section
  • v1.4 - October 26, 2017 - Added table of contents for easy navigation; added code generator quicktype.io; added serialization library Utf8Json
  • v1.5 - August 13, 2022 - Updated article title; added new project WpfUserAgent + updated article; fixed broken image links in MovieDB_MultiSearch.Json data file
  • v1.51 - September 3, 2022 - Added JsonCrack & JsonHero tools to the Viewers & Validators section.
  • v1.52 - November 1, 2022 - Link added to the third article: Deserializing Json Streams using Newtonsoft.Json & System.Text.Json with C# & VB

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionNewtonsoft.Json & Composite Pattern Pin
Oscar K.23-Nov-22 2:16
Oscar K.23-Nov-22 2:16 
PraiseGreat article Pin
Garry Lowther2-Nov-22 1:19
Garry Lowther2-Nov-22 1:19 
GeneralRe: Great article Pin
Graeme_Grant2-Nov-22 10:32
mvaGraeme_Grant2-Nov-22 10:32 
QuestionBroken link Pin
Reggie Van Wyk23-Aug-22 10:47
professionalReggie Van Wyk23-Aug-22 10:47 
AnswerRe: Broken link [updated] Pin
Graeme_Grant23-Aug-22 14:13
mvaGraeme_Grant23-Aug-22 14:13 
GeneralMy vote of 5 Pin
Member 1370414315-Aug-22 2:49
Member 1370414315-Aug-22 2:49 
GeneralMy vote of 5 Pin
LightTempler12-Aug-22 9:47
LightTempler12-Aug-22 9:47 
GeneralRe: My vote of 5 Pin
Graeme_Grant12-Aug-22 10:12
mvaGraeme_Grant12-Aug-22 10:12 
Generalusing JSON Pin
jay patel Jul202210-Jul-22 22:01
jay patel Jul202210-Jul-22 22:01 
Praiseexcellent! Pin
Southmountain30-May-22 7:04
Southmountain30-May-22 7:04 
GeneralRe: excellent! Pin
Graeme_Grant30-May-22 15:11
mvaGraeme_Grant30-May-22 15:11 
GeneralRe: excellent! Pin
Southmountain30-May-22 17:55
Southmountain30-May-22 17:55 
QuestionJSON Array Object to WEB API Pin
Member 149476136-Feb-22 12:10
Member 149476136-Feb-22 12:10 
AnswerRe: JSON Array Object to WEB API Pin
Graeme_Grant6-Feb-22 12:28
mvaGraeme_Grant6-Feb-22 12:28 
GeneralMy vote of 4 Pin
Member 1408651017-Jan-22 2:05
Member 1408651017-Jan-22 2:05 
QuestionJson data with collections within the Document Pin
Member 1149819227-Apr-21 10:13
professionalMember 1149819227-Apr-21 10:13 
QuestionDynamic insertion Pin
VOQUI26-Apr-21 21:29
VOQUI26-Apr-21 21:29 
GeneralMy vote of 5 Pin
LightTempler25-Apr-21 4:26
LightTempler25-Apr-21 4:26 
GeneralMy vote of 5 Pin
fteixeira26-Sep-20 13:13
fteixeira26-Sep-20 13:13 
SuggestionWith VBA you can use Javascript to work with JSON Pin
Member 118816069-Jul-20 10:54
Member 118816069-Jul-20 10:54 
PraiseAppreciation Pin
Member 1120498322-Feb-20 11:49
Member 1120498322-Feb-20 11:49 
QuestionHow can I write public class for complex JSON string? Pin
Member 146746701-Dec-19 19:18
Member 146746701-Dec-19 19:18 
QuestionError: The name "JsonHelper" does not exist in current context (C#) Pin
Member 146746701-Dec-19 18:32
Member 146746701-Dec-19 18:32 
QuestionNamespace "imports support.vb" not found Pin
Specialist_Dave22-Oct-19 21:54
Specialist_Dave22-Oct-19 21:54 
Can you tell me where I can find the namespace "imports support.vb"?
QuestionFantastic Article Pin
gseeme30-Jun-19 10:30
gseeme30-Jun-19 10:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.