In this article, we are going to learn how to deserialize JSON into a dynamic object in C#.
JSON deserialization in C# refers to the process of forming .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 in such cases. This does not necessarily mean the use of the 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.
VIDEO: How to Deserialize JSON Into Dynamic Object in C#.
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.