How do I use JSON.NET to deserialize into nested/recursive Dictionary and List?

I need to deserialize a complex JSON blob into standard .NET containers for use in code that is not aware of JSON. It expects things to be in standard .NET types, specifically Dictionary<string, object> or List<object> where "object" can be primitive or recurse (Dictionary or List).

I cannot use a static type to map the results and JObject/JToken don't fit. Ideally, there would be some way (via Contracts perhaps?) to convert raw JSON into basic .NET containers.

I've search all over for any way to coax the JSON.NET deserializer into creating these simple types when it encounters "{}" or "[]" but with little success.

Any help appreciated!


If you just want a generic method that can handle any arbitrary JSON and convert it into a nested structure of regular .NET types (primitives, Lists and Dictionaries), you can use JSON.Net's LINQ-to-JSON API to do it:

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

public static class JsonHelper
{
    public static object Deserialize(string json)
    {
        return ToObject(JToken.Parse(json));
    }

    public static object ToObject(JToken token)
    {
        switch (token.Type)
        {
            case JTokenType.Object:
                return token.Children<JProperty>()
                            .ToDictionary(prop => prop.Name,
                                          prop => ToObject(prop.Value));

            case JTokenType.Array:
                return token.Select(ToObject).ToList();

            default:
                return ((JValue)token).Value;
        }
    }
}

You can call the method as shown below. obj will either contain a Dictionary<string, object>, List<object>, or primitive depending on what JSON you started with.

object obj = JsonHelper.Deserialize(jsonString);

One way to deserialize a json string recursively into dictionaries and lists with JSON.NET is to create a custom json converter class that derives from the JsonConverter abstract class provided by JSON.NET.

It is in your derived JsonConverter where you put the implementation of how an object should be written to and from json.

You can use your custom JsonConverter like this:

var o = JsonConvert.DeserializeObject<IDictionary<string, object>>(json, new DictionaryConverter());

Here is a custom JsonConverter I have used with success in the past to achieve the same goals as you outline in your question:

public class DictionaryConverter : JsonConverter {
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { this.WriteValue(writer, value); }

    private void WriteValue(JsonWriter writer, object value) {
        var t = JToken.FromObject(value);
        switch (t.Type) {
            case JTokenType.Object:
                this.WriteObject(writer, value);
                break;
            case JTokenType.Array:
                this.WriteArray(writer, value);
                break;
            default:
                writer.WriteValue(value);
                break;
        }
    }

    private void WriteObject(JsonWriter writer, object value) {
        writer.WriteStartObject();
        var obj = value as IDictionary<string, object>;
        foreach (var kvp in obj) {
            writer.WritePropertyName(kvp.Key);
            this.WriteValue(writer, kvp.Value);
        }
        writer.WriteEndObject();
    }

    private void WriteArray(JsonWriter writer, object value) {
        writer.WriteStartArray();
        var array = value as IEnumerable<object>;
        foreach (var o in array) {
            this.WriteValue(writer, o);
        }
        writer.WriteEndArray();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
        return ReadValue(reader);
    }

    private object ReadValue(JsonReader reader) {
        while (reader.TokenType == JsonToken.Comment) {
            if (!reader.Read()) throw new JsonSerializationException("Unexpected Token when converting IDictionary<string, object>");
        }

        switch (reader.TokenType) {
            case JsonToken.StartObject:
                return ReadObject(reader);
            case JsonToken.StartArray:
                return this.ReadArray(reader);
            case JsonToken.Integer:
            case JsonToken.Float:
            case JsonToken.String:
            case JsonToken.Boolean:
            case JsonToken.Undefined:
            case JsonToken.Null:
            case JsonToken.Date:
            case JsonToken.Bytes:
                return reader.Value;
            default:
                throw new JsonSerializationException
                    (string.Format("Unexpected token when converting IDictionary<string, object>: {0}", reader.TokenType));
        }
    }

    private object ReadArray(JsonReader reader) {
        IList<object> list = new List<object>();

        while (reader.Read()) {
            switch (reader.TokenType) {
                case JsonToken.Comment:
                    break;
                default:
                    var v = ReadValue(reader);

                    list.Add(v);
                    break;
                case JsonToken.EndArray:
                    return list;
            }
        }

        throw new JsonSerializationException("Unexpected end when reading IDictionary<string, object>");
    }

    private object ReadObject(JsonReader reader) {
        var obj = new Dictionary<string, object>();

        while (reader.Read()) {
            switch (reader.TokenType) {
                case JsonToken.PropertyName:
                    var propertyName = reader.Value.ToString();

                    if (!reader.Read()) {
                        throw new JsonSerializationException("Unexpected end when reading IDictionary<string, object>");
                    }

                    var v = ReadValue(reader);

                    obj[propertyName] = v;
                    break;
                case JsonToken.Comment:
                    break;
                case JsonToken.EndObject:
                    return obj;
            }
        }

        throw new JsonSerializationException("Unexpected end when reading IDictionary<string, object>");
    }

    public override bool CanConvert(Type objectType) { return typeof(IDictionary<string, object>).IsAssignableFrom(objectType); }
}

Here is the equivalent in f#:

type IDictionaryConverter() =
    inherit JsonConverter()

    let rec writeValue (writer: JsonWriter) (value: obj) =
            let t = JToken.FromObject(value)
            match t.Type with
            | JTokenType.Object -> writeObject writer value
            | JTokenType.Array -> writeArray writer value
            | _ -> writer.WriteValue value    

    and writeObject (writer: JsonWriter) (value: obj) =
        writer.WriteStartObject ()
        let obj = value :?> IDictionary<string, obj>
        for kvp in obj do
            writer.WritePropertyName kvp.Key
            writeValue writer kvp.Value
        writer.WriteEndObject ()    

    and writeArray (writer: JsonWriter) (value: obj) = 
        writer.WriteStartArray ()
        let array = value :?> IEnumerable<obj>
        for o in array do
            writeValue writer o
        writer.WriteEndArray ()

    let rec readValue (reader: JsonReader) =
        while reader.TokenType = JsonToken.Comment do
            if reader.Read () |> not then raise (JsonSerializationException("Unexpected token when reading object"))

        match reader.TokenType with
        | JsonToken.Integer
        | JsonToken.Float
        | JsonToken.String
        | JsonToken.Boolean
        | JsonToken.Undefined
        | JsonToken.Null
        | JsonToken.Date
        | JsonToken.Bytes -> reader.Value
        | JsonToken.StartObject -> readObject reader Map.empty
        | JsonToken.StartArray -> readArray reader []
        | _ -> raise (JsonSerializationException(sprintf "Unexpected token when reading object: %O" reader.TokenType))


    and readObject (reader: JsonReader) (obj: Map<string, obj>) =
        match reader.Read() with
        | false -> raise (JsonSerializationException("Unexpected end when reading object"))
        | _ -> reader.TokenType |> function
            | JsonToken.Comment -> readObject reader obj
            | JsonToken.PropertyName ->
                let propertyName = reader.Value.ToString ()
                if reader.Read() |> not then raise (JsonSerializationException("Unexpected end when reading object"))
                let value = readValue reader
                readObject reader (obj.Add(propertyName, value))
            | JsonToken.EndObject -> box obj
            | _ -> raise (JsonSerializationException(sprintf "Unexpected token when reading object: %O" reader.TokenType))

    and readArray (reader: JsonReader) (collection: obj list) =
        match reader.Read() with
        | false -> raise (JsonSerializationException("Unexpected end when reading array"))
        | _ -> reader.TokenType |> function
            | JsonToken.Comment -> readArray reader collection
            | JsonToken.EndArray -> box collection
            | _ -> collection @ [readValue reader] |> readArray reader

    override __.CanConvert t = (typeof<IDictionary<string, obj>>).IsAssignableFrom t
    override __.WriteJson (writer:JsonWriter, value: obj, _:JsonSerializer) = writeValue writer value 
    override __.ReadJson (reader:JsonReader, _: Type, _:obj, _:JsonSerializer) = readValue reader

I love AutoMapper and seem to think it solves many problems... like this one...

why not just let the JSON.NET convert the thing into whatever it wants to... and use AutoMapper to map it into the object you really want.

Unless performance is paramount this extra step should be worth it for the reduction in complexity and the ability to use the serializer you want.