Updated on
Deserializing JSON into a dynamic object means skipping the class: we hand the JSON string to the serializer, ask for dynamic, and read properties by name at runtime. It is what we reach for when the payload’s shape is unknown, changes often, or when we only want two fields out of forty.
The two libraries answer that request differently, and the difference decides the approach. JsonConvert.DeserializeObject<dynamic>() returns a Newtonsoft JObject, whose properties really are reachable by name. JsonSerializer.Deserialize<dynamic>() returns a boxed JsonElement, which compiles fine and then throws the moment we touch .Genre. When the shape is known and stable, we should deserialize it into a class instead.
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
dynamicdeclarations - Using Anonymous Object
- Using the JSON DOM.
How Do We Deserialize JSON Into a Dynamic Object?
Dynamic deserialization means asking the serializer for dynamic instead of a class, then reading properties by name at runtime.
Newtonsoft supports this directly. JsonConvert.DeserializeObject<dynamic>(json) returns a JObject, and the runtime binder resolves movie.Genre and movie.Rating.Imdb against it. A JSON name that is not a valid C# identifier, such as Rotten Tomatoes, comes out through the indexer instead.
System.Text.Json does not support it. JsonSerializer.Deserialize<dynamic>(json) compiles and runs, but the value it hands back is a boxed JsonElement. Touching .Genre on that throws, because JsonElement has no such member and nothing dynamic behind it to resolve one.
So the choice is not a matter of taste. If we want member access by name, we use Newtonsoft. If we want the native library, we accept its DOM API and call GetProperty() instead, which is what the rest of this article does.
Member access works here because JObject implements IDynamicMetaObjectProvider, which is the same mechanism behind the dynamic type in C# everywhere else in the language.
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. For this payload the call returns a JObject; what it returns for an array, a scalar or null is covered in full below.
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. Note the two different access paths in the block above: dynamicObject.Rating.Imdb binds at runtime, while Rating["Rotten Tomatoes"] has to use the indexer, because that name cannot be a member-access expression in C# at all.
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. If it is not obvious which of these three types to reach for, we cover how ExpandoObject, DynamicObject and dynamic differ separately.
Using dynamic With System.Text.Json
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. The question of whether dynamic should work against the native JSON types was raised and closed as a design discussion in 2021, and nothing has moved on it since.
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.Throws<RuntimeBinderException>(() => 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 a RuntimeBinderException 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.
That is documented behaviour rather than an accident. Microsoft’s migration guide from Newtonsoft.Json states it in one line: “System.Text.Json stores a boxed JsonElement for both primitive and complex values whenever deserializing to Object”. A dynamic type argument is object to the serializer, so that is the rule we are hitting.
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.
What Does DeserializeObject<dynamic>() Actually Return?
JsonConvert.DeserializeObject<dynamic>() has no single return type. What comes back depends entirely on the JSON we passed in, and for half the cases it is not a Newtonsoft type at all.
A JSON object gives us a JObject and a JSON array gives us a JArray. A bare scalar does not give us a JValue, as the type names suggest: "Thriller" arrives as a plain string, 8.1 as a double, true as a bool. The JSON literal null gives us an actual null reference, so a null-forgiving operator on that line hides a failure rather than preventing one.
System.Text.Json collapses every case into one type. JsonSerializer.Deserialize<dynamic>() returns a boxed JsonElement, and its ValueKind property tells us which shape arrived. Only the null literal escapes that, coming back as a null reference here too.
Asking for JToken instead of dynamic buys uniformity: every payload becomes a JToken, scalars become JValue, and even null arrives as a token. That is a different contract, not a free one.
| JSON we pass in | JsonConvert.DeserializeObject<dynamic>() | JsonConvert.DeserializeObject<JToken>() | JsonSerializer.Deserialize<dynamic>() |
|---|---|---|---|
| { "Genre": "Thriller" } | JObject | JObject | JsonElement, ValueKind.Object |
| [ 1, 2, 3 ] | JArray | JArray | JsonElement, ValueKind.Array |
| "Thriller" | string | JValue, JTokenType.String | JsonElement, ValueKind.String |
| 8.1 | double | JValue, JTokenType.Float | JsonElement, ValueKind.Number |
| true | bool | JValue, JTokenType.Boolean | JsonElement, ValueKind.True |
| null | null - a real null reference | JValue, JTokenType.Null | null - a real null reference |
| Member access, obj.Genre | works on JObject only | n/a - statically typed | throws RuntimeBinderException |
Each row of that table is asserted by a test in the sample solution, so the type names stay honest as the libraries move.
How Do We Deserialize JSON Into an Anonymous Type?
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);
}
How Do We Read JSON With the DOM APIs?
Both the native and the Newtonsoft library offer a DOM API to retrieve data from a JSON string on demand. Both of them have several DOM classes that work in pairs. For example, the native library provides JsonElement/JsonDocument combinations for readonly DOM and JsonNode/JsonObject pair for mutable DOM. Newtonsoft similarly uses JToken/JObject. The DOM route is also the one to take for deeply nested payloads with a known shape, and for iterating over a JSON object’s properties when we do not know their names in advance. If the payload is not a string yet, we cover reading the JSON out of a file in the first place separately, including the untyped JsonNode and JsonObject route.
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 the same indexer chain applies to JsonNode, which is its base type.
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 use the JSON Path query API through 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.
Newtonsoft has the wider dynamic-shaped API surface here: true member access and JSON Path have no native equivalent. System.Text.Json is the platform default, and its readonly DOM is the fastest route of any we have shown, which the next section measures.
Which Dynamic Deserialization Approach Is Fastest?
JsonElement is the fastest way to pull values out of JSON we have no class for, and ExpandoObject is the slowest by a wide margin.
On the sample data, read-only JsonElement comes first. Newtonsoft’s anonymous type, the native anonymous type and the mutable JsonObject DOM follow close behind, all inside twice the baseline. JObject, JSON Path and Newtonsoft’s dynamic sit together at six to seven times the baseline, and ExpandoObject is far out on its own at sixty-five times, roughly ten times slower than the next slowest approach.
The shape of that ranking matters more than the exact figures. JsonElement reads values straight out of the parsed buffer without building a node graph, while JObject and dynamic construct a full mutable tree first. The anonymous-type routes fall between the two: the serializer fills the fields we declared and builds no DOM at all.
For a hot path, JsonElement is the default and ExpandoObject is the one to avoid.
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, the MovieStats.json file that ships with the benchmark project.
The table below is the current run on .NET 10. The code it measures is unchanged apart from one fix: the Newtonsoft anonymous-type benchmark used to call a different method from the native one it was compared against, so that row was not a like-for-like comparison and now is.
Once we run the benchmark, we can inspect the result:
| Method | Categories | Mean | Error | StdDev | Median | Ratio | RatioSD | |---------------------------- |--------------- |------------:|----------:|----------:|------------:|------:|--------:| | UsingJsonElement | SystemTextJson | 495.6 us | 2.14 us | 1.90 us | 495.9 us | 1.00 | 0.01 | | NewtonsoftJsonAnonymousType | NewtonsoftJson | 776.9 us | 14.64 us | 12.98 us | 774.7 us | 1.57 | 0.03 | | SystemTextJsonAnonymousType | SystemTextJson | 857.6 us | 8.45 us | 7.91 us | 853.6 us | 1.73 | 0.02 | | UsingJsonObject | SystemTextJson | 933.7 us | 17.70 us | 18.17 us | 934.2 us | 1.88 | 0.04 | | UsingJObject | NewtonsoftJson | 3,122.4 us | 100.01 us | 294.89 us | 3,211.0 us | 6.30 | 0.59 | | UsingJsonPath | NewtonsoftJson | 3,199.0 us | 62.62 us | 97.49 us | 3,206.0 us | 6.45 | 0.20 | | UsingDynamic | NewtonsoftJson | 3,333.1 us | 120.92 us | 356.52 us | 3,448.9 us | 6.73 | 0.72 | | UsingExpandoObject | NewtonsoftJson | 32,205.1 us | 615.70 us | 604.70 us | 32,072.5 us | 64.98 | 1.21 |
The readonly native DOM wins outright. Beyond that the split is not simply native-versus-Newtonsoft: with both anonymous-type benchmarks now doing the same work, Newtonsoft’s is the faster of the pair, while Newtonsoft’s DOM and dynamic routes are six to seven times the baseline. The one unambiguous rule is to avoid ExpandoObject for this job.
Conclusion
In this article, we have explored a few ways to deserialize JSON into a dynamic object. Newtonsoft is the library to pick when we want member access by name, and the native readonly DOM is the one to pick when we want speed.
Tested with .NET 10 and Newtonsoft.Json 13.0.4.

I have elaborated on the example above, and think that the following piece of code would be helpful for some people in the future:
Dictionary dic = new Dictionary();
JObject dynamicObject = JsonConvert.DeserializeObject(jsonString);
IEnumerable list = ((IEnumerable)dynamicObject).ToList();
foreach (JToken token in list)
{
string key = ((JProperty)token).Path;
string value = ((JProperty)token).Value.ToString();
dic.Add(key, value);
}
Hi Ahmad, thanks for your feedback.
Of course, once we convert to JObject, we can play with it in many ways, like how you fill out a dictionary. That said, you may also need recursion if your desired data lies in the deep level of the graph.
By the way, the sample code you shared does not compile.
You can do the samething without using Json.net nuget package with internal System.Text.Json library by using ExpandoObject
Apparently it’s possible to deserialize an anonymous object using System.Text.Json
https://stackoverflow.com/questions/59313256/deserialize-anonymous-type-with-system-text-json
Thank you for that suggestion. We’ve updated the article. Just clear the cache (CTRL+F5) if you don’t see the changes. One more time, thanks a lot for this.