In this article, we are going to learn how to deserialize JSON into a dynamic object in C#.

To download the source code for this article, you can visit our GitHub repository.

JSON deserialization in C# refers to the process of forming up .NET objects from a JSON string. Most of the time, this means creating strongly-typed POCOs. However, there are certain situations when we may prefer flexibility over type-inference. For example, cherry-picking a small portion of JSON data, dealing with external JSON data whose structure is largely unknown or changes very often, etc. Dynamic deserialization comes into play for such cases. This does not necessarily mean the use of language’s inbuilt dynamic keyword. There are other ways as well.

We are going to see how we can do this using the native System.Text.Json library and the popular Newtonsoft.Json library.

Preparation of JSON Data Source

Let’s assume we have to extract genre and rating information from movie-stats data coming as a JSON string:

public class MovieStats
{
    public static string SquidGame => @"
    {
        ""Name"": ""Squid Game"",
        ""Genre"": ""Thriller"",
        ""Rating"": {
            ""Imdb"": 8.1,
            ""Rotten Tomatoes"": 0.94
        },
        ""Year"": 2021,
        ""Stars"": [""Lee Jung-jae"", ""Park Hae-soo""],
        ""Language"": ""Korean"",
        ""Budget"": ""$21.4 million""
    }";
}

From the stats of SquidGame movie, we only want to cherry-pick “Genre” and rating of “IMDb” or “Rotten Tomatoes”.  

We have three path-ways to achieve this:

  • Use of dynamic declarations
  • Using Anonymous Object
  • Leverage the power of JSON DOM.

Let’s dive into the deep!

Deserialize JSON Into Dynamic Object Using dynamic

First of all, we want to explore the dynamic way. Newtonsoft library is quite convenient in this regard having long-time support for dynamic. On the other hand, the native library has a different story, and we will discuss it later.

So, let’s start with the Newtonsoft library.

Using dynamic With Newtonsoft.Json

We are going to add a GenreRatingFinder helper class in our class library project that holds our first deserialization routine:

// Newtonsoft/GenreRatingFinder.cs
public static class GenreRatingFinder
{
    public static (string? Genre, double Imdb, double Rotten) UsingDynamic(string jsonString)
    {
        var dynamicObject = JsonConvert.DeserializeObject<dynamic>(jsonString)!;

        var genre = dynamicObject.Genre;
        var imdb = dynamicObject.Rating.Imdb;
        var rotten = dynamicObject.Rating["Rotten Tomatoes"];

        return (genre, imdb, rotten);
    }
}

Like always we use the JsonConvert class for the deserialization. A call to the DeserializeObject<dynamic> method gives us a plain object instance. 

Under the hood, this object holds all the properties from the JSON tree. Because of dynamic declaration, we can directly access Genre and Rating properties from there. We can even access the nested property Rating.Imdb in a natural way. Even more, if we can’t directly access a JSON property by its name due to incompatibility with C# property-name like “Rotten Tomatoes”, we can access it as a dictionary item. Pretty convenient, right?

As a side note, we use the null-forgiving operator (!) here to keep syntax clean and short. We will continue using it in this article where relevant. However, you should be cautious about using this in a real application.

Once we apply this helper method on MovieStats.SquidGame:

// NewtonsoftJsonUnitTest.cs
var jsonString = MovieStats.SquidGame;

var (genre, imdb, rotten) = GenreRatingFinder.UsingDynamic(jsonString);

Assert.Equal("Thriller", genre);
Assert.Equal(8.1d, imdb);
Assert.Equal(0.94d, rotten);

We get the result we desire. This is the most popular and widely used way for dynamic deserialization with Newtonsoft.

Using ExpandoObject With Newtonsoft.Json

Pretty often a dynamic object corresponds to an ExpandoObject of System.Dynamic namespace:

// Newtonsoft/GenreRatingFinder.cs
public static (string? Genre, double Imdb, double Rotten) UsingExpandoObject(string jsonString)
{
    dynamic dynamicObject = JsonConvert.DeserializeObject<ExpandoObject>(jsonString)!;

    var genre = dynamicObject.Genre;
    var imdb = dynamicObject.Rating.Imdb;

    IDictionary<string, object> rating = dynamicObject.Rating;
    var rotten = (double)rating["Rotten Tomatoes"];

    return (genre, imdb, rotten);
}

Similar to the dynamic version, we can invoke the JsonConvert.DeserializeObject method with ExpandoObject type argument.

The subsequent section resembles the previous routine until we hit the “Rotten Tomatoes” part. For this part, we need to cast its parent object (Rating) to a dictionary. This allows us to retrieve the value of “Rotten Tomatoes” by key. It’s a bit inconvenient way but worth the effort if you’re a big fan of ExpandoObject.

Using dynamic With System.Text.Json to Deserialize JSON Into a Dynamic Object

Now is the time to go with the native library.

In the legacy ASP.NET MVC application, we would get a Dictionary<string, object> when using dynamic with the native deserializer class: JavaScriptSerializer. That was not a true dynamic thing of course, but surely offered a bit of flexibility in managing.

However, the flexibility of dynamic comes at the price of performance. That’s why the .NET team set dynamic aside the design considerations of System.Text.Json as they want this to come out as a high-performant library.  And that’s not the only thing. dynamic is currently flagged as an “archived component”. That means it will not evolve with the latest language features for the time being. Check out this thread for more details.

So, we’re not getting dynamic support in the native JSON library in near future. That said, the deserializer does not complain if we use dynamic anyway:

// NativeJsonUnitTest.cs
var jsonString = MovieStats.SquidGame;

var dynamicObject = JsonSerializer.Deserialize<dynamic>(jsonString)!;

Assert.ThrowsAny<Exception>(() => dynamicObject.Genre);
Assert.IsType<JsonElement>(dynamicObject);

As we see, we can form a dynamic object using the JsonSerializer.Deserialize method. However, this object does not recognize the Genre or Rating property and throws an error if we try. Because, under the hood, this is a boxed JsonElement, a type that is the building block of native JSON DOM. So, we don’t have the convenience to use it in a truly dynamic way.

In short, using dynamic with the native deserializer has no added benefit and results in a JSON DOM which has its own API to deal with.

Dynamic Deserialization of JSON Using Anonymous Object

Another convenient way of deserialization with Newtonsoft is to use the anonymous object:

// Newtonsoft/GenreRatingFinder.cs
public static (string? Genre, double Imdb) UsingAnonymousType(string jsonString)
{
    var anonymous = JsonConvert.DeserializeAnonymousType(jsonString, new 
    { 
        Genre = string.Empty, 
        Rating = new { Imdb = 0d } 
    })!;

    var genre = anonymous.Genre;
    var imdb = anonymous.Rating.Imdb;

    return (genre, imdb);
}

Once again, we come up with an elegant solution in a few simple steps. We call the JsonConvert.DeserializeAnonymousType method along with an anonymous object. This anonymous object essentially needs to be a blueprint of our target JSON graph. That’s why we specify the Genre property with an initial value of an empty string. Similarly, we specify and initialize the nested property Rating.Imdb as double. That does the trick!

The resulting object holds the target JSON data as we want. From there, we can access the Genre and Rating.Imdb properties in a strongly-typed way!

If we want to get the value of “Rotten Tomatoes”, we can do that too:

public static (string? Genre, double Imdb, double Rotten) UsingAnonymousTypeWithDictionary(string jsonString)
{
    var anonymous = JsonConvert.DeserializeAnonymousType(jsonString, new
    {
        Genre = string.Empty,
        Rating = new Dictionary<string, double>()
    })!;

    var genre = anonymous.Genre;
    var imdb = anonymous.Rating["Imdb"];
    var rotten = anonymous.Rating["Rotten Tomatoes"];

    return (genre, imdb, rotten);
}

Again, we just need to hint at the deserializer that Rating is a dictionary. From there, we can easily pick the values by key.

In the case of the native library, we don’t have any direct method for the anonymous type. But, we can implement it on our own:

static T DeserializeAnonymousType<T>(string jsonString, T anonymousObject)
    => JsonSerializer.Deserialize<T>(jsonString)!;

This is a bit tricky part. We prepare a generic method that works on type inference. Since we aim to call this method anonymously i.e. without specifying the generic type argument, we need a parameter that infers the type during invocation. That’s the role the anonymousObject parameter plays here. The rest is nothing but calling the usual deserializing method.

With this helper method, we can work the same way as the Newtonsoft version:

public static (string? Genre, double Imdb) UsingAnonymousType(string jsonString)
{
    var anonymous = DeserializeAnonymousType(jsonString, new
    {
        Genre = string.Empty,
        Rating = new { Imdb = 0d }
    })!;

    var genre = anonymous.Genre;
    var imdb = anonymous.Rating.Imdb;

    return (genre, imdb);
}

Deserialize JSON Into Dynamic Object Using JSON DOM

Both native and Newtonsoft library offers strong DOM API to retrieve data from JSON string on demand. Both of them has several DOM classes that work in pair and can be alternatively used. For example, the native library provides JsonElement/JsonDocument combinations for readonly DOM and JsonNode/JsonObject pair for mutable DOM. Newtonsoft similarly uses JToken/JObject.

Using JSON DOM With System.Text.Json

First, let’s talk about our already familiar type JsonElement. We are going to implement a helper method in the native version of the GenreRatingFinder class:

// Native/GenreRatingFinder.cs
public static (string? Genre, double Imdb, double Rotten) UsingJsonElement(string jsonString)
{
    var jsonElement = JsonSerializer.Deserialize<JsonElement>(jsonString);

    return FromJsonElement(jsonElement);
}

private static (string? Genre, double Imdb, double Rotten) FromJsonElement(JsonElement jsonElement)
{
    var genre = jsonElement
        .GetProperty("Genre")
        .GetString();

    var imdb = jsonElement
        .GetProperty("Rating")
        .GetProperty("Imdb")
        .GetDouble();

    var rotten = jsonElement
        .GetProperty("Rating")
        .GetProperty("Rotten Tomatoes")
        .GetDouble();

    return (genre, imdb, rotten);
}

We simply deserialize to JsonElement as we do for POCO. All we get here is a DOM tree of nodes – each node representing the corresponding node of JSON data structure.

Next, we call our FromJsonElement helper method that retrieves Genre, Imdb, and Rotten Tomatoes traversing down the DOM tree. 

Inside this method, we use the GetProperty method of JsonElement. This method looks for a descendant node by name. We find the Genre node in the first layer of descendants. Similarly, we reach the Rating.Imdb node in the second layer by chain invocations of GetProperty method. The same goes for the Rotten Tomatoes node. On reaching each node, we can obtain the value according to the target data type e.g. GetString for string value, GetDouble for double value, etc. That’s it.

A similar approach is applicable for JsonDocument:

public static (string? Genre, double Imdb, double Rotten) UsingJsonDocument(string jsonString)
{
    //using var jsonDocument = JsonSerializer.Deserialize<JsonDocument>(jsonString)!;
    using var jsonDocument = JsonDocument.Parse(jsonString);

    return FromJsonElement(jsonDocument.RootElement);
}

Though we can use the usual JsonSerializer.Deserialize method, we go for a slightly faster alternative: JsonDocument.Parse method. Since JsonDocument is disposable we also declare a using block. Subsequently, we pass the RootElement (an instance of JsonElement) to the FromJsonElement method for the final output.

Using Mutable JSON DOM With System.Text.Json

As mentioned before, the native library provides another set of DOM classes JsonNode/JsonObject. They’re a bit slower but more convenient than their JsonElement/JsonDocument counterparts:

// Native/GenreRatingFinder.cs
public static (string? Genre, double Imdb, double Rotten) UsingJsonObject(string jsonString)
{
    var jsonDom = JsonSerializer.Deserialize<JsonObject>(jsonString)!; 

    var genre = (string)jsonDom["Genre"]!;
    var imdb = (double)jsonDom["Rating"]!["Imdb"]!;
    var rotten = (double)jsonDom["Rating"]!["Rotten Tomatoes"]!;

    return (genre, imdb, rotten);
}

Here, the deserialization part is nothing special. But the data retrieval part is quite interesting. We can access all the data in a nice chain of index notations!

Our example is for JsonObject, but it also applies to JsonNode – you can see it for yourself in our source code.

Using JSON DOM With Newtonsoft.Json

Newtonsoft also provides a similar elegant API with their JObject/JToken DOM classes:

// Newtonsoft/GenreRatingFinder.cs
public static (string? Genre, double Imdb, double Rotten) UsingJObject(string jsonString)
{
    var jsonDom = JsonConvert.DeserializeObject<JObject>(jsonString)!;

    var genre = (string)jsonDom["Genre"]!;
    var imdb = (double)jsonDom["Rating"]!["Imdb"]!;
    var rotten = (double)jsonDom["Rating"]!["Rotten Tomatoes"]!;

    return (genre, imdb, rotten);
}

This is no different than the native version except for the deserialization part. We can also use JToken in place of JObject.

Unlike the native version, Newtonsoft also supports a path-based node selection:

// Newtonsoft/GenreRatingFinder.cs
public static (string? Genre, double Imdb, double Rotten) UsingJsonPath(string jsonString)
{
    var jsonDom = JsonConvert.DeserializeObject<JObject>(jsonString)!;

    var genre = (string)jsonDom.SelectToken("$.Genre")!;
    var imdb = (double)jsonDom.SelectToken("$.Rating.Imdb")!;
    var rotten = (double)jsonDom.SelectToken("$.Rating['Rotten Tomatoes']")!;

    return (genre, imdb, rotten);
}

Here, we leverage the power of JSON Path/Query API using the SelectToken method. This is particularly useful if we want to cherry-pick data based on the value of some other node of the tree.

Overall, Newtonsoft is quite feature-rich in all aspects of dynamic JSON deserialization as compared to System.Text.Json.

Benchmark Analysis

We now have a few variants of dynamic deserialization routines. It’s time for benchmarking these methods. We’re going to use a bigger JSON data source for this purpose.

Once we run the benchmark, we can inspect the result:

|                      Method |     Categories |      Mean |     Error |    StdDev | Ratio | RatioSD |
|---------------------------- |--------------- |----------:|----------:|----------:|------:|--------:|
|            UsingJsonElement | SystemTextJson |  1.080 ms | 0.0077 ms | 0.0064 ms |  1.00 |    0.00 |
| NewtonsoftJsonAnonymousType | NewtonsoftJson |  1.827 ms | 0.0259 ms | 0.0230 ms |  1.69 |    0.02 |
|             UsingJsonObject | SystemTextJson |  1.880 ms | 0.0355 ms | 0.0349 ms |  1.74 |    0.04 |
| SystemTextJsonAnonymousType | SystemTextJson |  2.717 ms | 0.0071 ms | 0.0059 ms |  2.51 |    0.02 |
|                UsingJObject | NewtonsoftJson |  5.991 ms | 0.1035 ms | 0.0918 ms |  5.53 |    0.06 |
|               UsingJsonPath | NewtonsoftJson |  6.090 ms | 0.1186 ms | 0.1109 ms |  5.65 |    0.12 |
|                UsingDynamic | NewtonsoftJson |  6.192 ms | 0.1203 ms | 0.1337 ms |  5.75 |    0.16 |
|          UsingExpandoObject | NewtonsoftJson | 84.614 ms | 0.5040 ms | 0.3935 ms | 78.31 |    0.47 |

We can see that the native library is way faster than Newtonsoft variants, especially when using the readonly DOM. Another important thing is that we should avoid using ExpandoObject for such purposes.

Conclusion

In this article, we have explored a few ways to deserialize JSON into a dynamic object. In the end, our performance analysis shows that System.Text.Json performs better than Newtonsoft.Json in general.