Updated on
JObject is Json.NET’s in-memory model of a JSON object, and it behaves like a dictionary: jsonObject["price"] hands back the value stored under that key. What comes back is a JToken, never a string or an int, so every read ends in a conversion.
Json.NET gives us four ways to make that read. Three of them differ only in how they handle nesting and how loudly they fail. The fourth, TryGetValue(), does not fail at all.
VIDEO: 4 Essential Ways to Get a Value from JObject.
What Is JObject in C#?
JObject is the Json.NET class that holds a JSON object in memory. It implements IDictionary<string, JToken>, so it is indexed by key, it has a Count, and it can be enumerated property by property.
JObject.Parse() turns a JSON string into one, and every read in this article then works on the tree it built.
The values in that tree are never plain C# types. A lookup returns a JToken, the abstract base of every node Json.NET creates: a JValue for a string or a number, a nested JObject for an object, a JArray for an array.
That is why every read here ends in a conversion. We either cast the token, or we call Value<T>() and spell the target type out in the call.
Keys are case-sensitive, and a key that is not there is not an error on its own. What happens next depends entirely on which of the four approaches we picked.
Json.NET’s node types form a small hierarchy, and knowing which one we are holding explains every cast in this article:

We also covered how to iterate over a JSON array and how to deserialize JSON into a dynamic object using Json.NET. If the project is on System.Text.Json rather than Json.NET, JsonNode is the equivalent model and reads the same way: node["price"]["amount"].GetValue<int>(). We cover it in reading and parsing JSON files with System.Text.Json.
Data Preparation
Our main focus for this article is learning how to read data from a JSON object using JObject. So let’s create a simple console application and install the Json.NET library using the command:
NuGet\Install-Package Newtonsoft.Json
After that, let’s add a new TestData class with a single method:
public string GenerateSingleJsonObject()
{
const string car = """
{
"name": "Charger",
"make": "Dodge",
"model": "RT",
"year": 2019,
"price": {
"amount": 36100,
"currency": "USD"
}
}
""";
return car;
}
We have a method that returns a single JSON object as a string.
Each of the four approaches below parses this string with JObject.Parse() and then gets values from it by key. JObject.Parse() throws a JsonReaderException when the text is not valid JSON, so for input from outside our code it is worth knowing how to check that a string is valid JSON before parsing it.
How Do We Read a Value Out of a JObject?
Four calls read a value out of a JObject, and they differ in two ways only: how they handle nesting, and what they do when the key is not there.
The indexer is the shortest. jsonObject["name"] returns a JToken we cast, and reaching a nested value means casting the intermediate node to JObject first.
Value<T>() puts the type in the call instead of in a cast, so jsonObject.Value<int>("year") is one expression. Nested values chain: Value<JObject>("price").Value<int>("amount").
SelectToken() takes a path, so the whole descent is one string: SelectToken("price.amount"). It also accepts JSONPath, which is what makes it the right choice once the JSON stops being flat.
The first three assume the key is there. TryGetValue() is the only one that treats a missing key as an ordinary outcome: it returns false and leaves the token null instead of throwing or handing back a default.
These four approaches only read the JSON. To add a property to it, the same JObject lets us insert a key/value pair into existing JSON.
We will go through them in this order:
- Passing the key as an index
- Using the
JObject.Value<T>()method - Using the
JObject.SelectToken()method - Using the
JObject.TryGetValue()method
First, let’s add a new JObjectManipulation class, which we’ll use to illustrate these four approaches:
public class JObjectManipulation
{
public string SingleJsonObject { get; set; }
public JObjectManipulation()
{
InitializeData();
}
public void InitializeData()
{
var testData = new TestData();
SingleJsonObject = testData.GenerateSingleJsonObject();
}
}
In this class, we are initializing the SingleJsonObject class property in the InitializeData() method, which we then call in the constructor.
Passing the Key as an Index
In the JObjectManipulation class, let’s add a new method:
public int GetValuesUsingIndex()
{
var jsonObject = JObject.Parse(SingleJsonObject);
var name = (string)jsonObject["name"];
var make = (string)jsonObject["make"];
var model = (string)jsonObject["model"];
var year = (int)jsonObject["year"];
var price = (JObject)jsonObject["price"];
var amount = (int)price["amount"];
var currency = (string)price["currency"];
Console.WriteLine($"A {make} {name} {model} {year} costs {amount} {currency} \n");
return jsonObject.Count;
}
We first convert the SingleJsonObject string to a JObject using JObject.Parse. To get values from the JSON object, we pass the keys as indexes to the JObject, using the square bracket notation. After getting the values, we cast them to the desired type.
The price key has nested JSON with amount and currency key-value pairs. To get these values, we follow the same steps, by first converting the value of price to a JObject using a cast:
var price = (JObject)jsonObject["price"]
Then, we access the values of amount and currency using the index of the resulting JObject:
var amount = (int)price["amount"]; var currency = (string)price["currency"];
If the JSON data is not deeply nested, we can use this method to get the values. However, if our JSON object has a deep hierarchy, accessing a value that is deep can be very tedious. That’s where the Value<T>() method comes into play.
Using the Value<T>() Method
Using this method, we directly pass the key as the parameter to the method. In addition to that, we also pass the appropriate type as a type argument to the Value<T> method. The method then returns the value already cast.
That said, let’s create a new GetValuesUsingValueMethod method:
public int GetValuesUsingValueMethod()
{
var jsonObject = JObject.Parse(SingleJsonObject);
var name = jsonObject.Value<string>("name");
var make = jsonObject.Value<string>("make");
var model = jsonObject.Value<string>("model");
var year = jsonObject.Value<int>("year");
var amount = jsonObject.Value<JObject>("price")
.Value<int>("amount");
var currency = jsonObject.Value<JObject>("price")
.Value<string>("currency");
Console.WriteLine($"A {make} {name} {model} {year} costs {amount} {currency} \n");
return jsonObject.Count;
}
We first convert the SingleJsonObject string to a JObject. Then, we use the Value<T>() method to get values from the JObject and assign them to local variables. In cases where we have nested JSON, we extract the JObject first and chain the call for the specific type.
Compared to the first approach, using the Value<T>() method is less tedious when working with a nested JSON object because we can chain our calls. However, to get the correct value, we have to pass the right key.
Using the SelectToken() Method
To continue, let’s add a new method to the JObjectManipulation class:
public int GetValuesUsingSelectToken()
{
var jsonObject = JObject.Parse(SingleJsonObject);
var name = (string)jsonObject.SelectToken("name");
var make = (string)jsonObject.SelectToken("make");
var model = (string)jsonObject.SelectToken("model");
var year = (int)jsonObject.SelectToken("year");
var amount = (int)jsonObject.SelectToken("price.amount");
var currency = (string)jsonObject.SelectToken("price.currency");
Console.WriteLine($"A {make} {name} {model} {year} costs {amount} {currency} \n");
return jsonObject.Count;
}
The first step is creating a JObject from the JSON string. After that, we call the SelectToken method passing the keys as parameters. Then, we cast the values to appropriate data types. The JSONPath Tester lets you try a query like this against your own JSON and see the matching values before you put it in code.
When working with more complex JSON data, the SelectToken() method would be the best choice of the three. Beyond basic usage, it offers some advanced capabilities like:
- Querying JSON arrays using indexes
- Support for JSONPath queries
- Support for LINQ queries
When we call each of these methods discussed, we get this output in the console:
A Dodge Charger RT 2019 costs 36100 USD
The three approaches so far disagree about what to do with a missing key, and that difference matters more than their syntax. TryGetValue() sidesteps the question entirely.
Using the JObject.TryGetValue() Method
The TryGetValue takes both the key and output variable as parameters. Then, it tries to get the JSON token that matches the specified key. If the token is found, this method returns true. Otherwise, it returns false.
Let’s demonstrate this:
public int GetValuesUsingTryGetValue()
{
JObject jsonObject = JObject.Parse(SingleJsonObject);
if (jsonObject.TryGetValue("name", out JToken nameToken))
{
string name = (string)nameToken;
Console.WriteLine($"Name: {name}");
}
if (jsonObject.TryGetValue("make", out JToken makeToken))
{
string make = (string)makeToken;
Console.WriteLine($"Make: {make}");
}
if (jsonObject.TryGetValue("price", out JToken priceToken) && priceToken is JObject priceObject)
{
if (priceObject.TryGetValue("amount", out JToken amountToken))
{
int amount = (int)amountToken;
Console.WriteLine($"Price amount: {amount}");
}
if (priceObject.TryGetValue("currency", out JToken currencyToken))
{
string currency = (string)currencyToken;
Console.WriteLine($"Price currency: {currency}");
}
}
return jsonObject.Count;
}
We are passing the keys for which we want to get values together with JToken objects. If the method returns true, we cast the JToken objects into specified types.
Similarly, we have a nested JSON price object. In this case, we first call the TryGetValue method passing the price key. If this returns true, we use the is operator to check if priceToken is a JObject. If this check returns true as well, we proceed to access the values of the price nested JSON object.
Calling this method, we get:
Name: Charger Make: Dodge Price amount: 36100 Price currency: USD
Using TryGetValue method, if any of the keys we are trying to access is missing, the method returns false. This way, we are not trying to access the values of a non-existent key.
The appropriate use case for this method is when working with JSON data from an API where the data does not have a fixed structure. In this case, if a key is missing, our application won’t break.
What Happens When the Key Is Missing or the Type Is Wrong?
A missing key does not behave the same way in all four approaches, and the difference is the reason to prefer one over another.
Casting the indexer result is the sharpest edge. (string)jsonObject["colour"] returns null, but (int)jsonObject["colour"] throws ArgumentNullException, because there is no token to convert.
Value<T>() never throws on a missing key. It returns the default for the type, so a missing int arrives as 0 and a missing string arrives as null, which is convenient until a real zero and an absent key become indistinguishable.
SelectToken() returns null for a path that matches nothing, and the same cast rules then apply. Passing true as its second argument changes that: it throws a JsonException naming the property instead.
A wrong type is a different failure. (int)jsonObject["name"] throws FormatException, and so does Value<int>("model"), because the token exists and the conversion is what fails. TryGetValue() avoids both, because it returns false for a missing key and converts nothing.
Every cell in this table comes from running the call against this article’s JSON:
| Approach | Key is missing | Key exists, wrong type | Use it when |
|---|---|---|---|
(string)jsonObject["key"] | returns null | a number comes back as text; a nested object throws ArgumentException | the JSON is shallow and the key is guaranteed |
(int)jsonObject["key"] | ArgumentNullException | FormatException on text that is not a number | same, but never on an optional key |
Value<string>("key") | returns null | a number comes back as text; a nested object throws InvalidCastException | reading a value type or chaining through one level of nesting |
Value<int>("key") | returns 0, the default | FormatException on text that is not a number | the same, and a silent 0 is acceptable |
(string)SelectToken(path) | returns null | a number comes back as text; a nested object throws ArgumentException | the value is several levels deep |
SelectToken("key", true) | JsonException | n/a | a missing key should stop the program |
TryGetValue("key", out var token) | returns false, token is null | no conversion happens yet | the JSON comes from an API and the shape is not guaranteed |
Only two of these reads stop the program on a missing key: the int cast on the indexer and SelectToken() with true. Every other read carries on with null, 0 or false, and the code after it has to notice.
How Do We Check Whether a Key Exists in a JObject?
ContainsKey() is the direct way to check. jsonObject.ContainsKey("price") returns true or false and converts nothing, which makes it the cheapest guard to put before any of the four reads.
It comes from IDictionary<string, JToken>, the interface JObject implements, and none of the four approaches earlier in this article calls it.
The lookup is case-sensitive. ContainsKey("price") is true on our JSON and ContainsKey("Price") is false, and the indexer agrees: jsonObject["Name"] is null even though name is there. Json.NET compares keys with an ordinal, case-sensitive comparison by design, and a capitalisation mismatch is the likeliest reason a read returns null on JSON that contains the key.
TryGetValue() and GetValue() both take a StringComparison overload, so there is no need to rewrite keys to match the code: jsonObject.TryGetValue("Name", StringComparison.OrdinalIgnoreCase, out var token) finds the property whatever the sender capitalised.
When the JSON carries both name and Name, an exact match still wins, and the comparison we pass only applies when there is none.
The Json.NET documentation for that overload states the order: “The exact property name will be searched for first”.
When the code downstream expects a plain Dictionary, we can convert a JObject to a Dictionary first.
Conclusion
A missing key is where the four reads differ most. The int cast on the indexer throws, Value<T>() returns the type’s default, and SelectToken() returns null unless we pass true to make it throw. TryGetValue() returns false and converts nothing, which suits JSON from an API that has no fixed structure. Once the JSON stops being flat, a SelectToken() path reaches a nested value in one string. Keys are case-sensitive, so a read that comes back null on JSON that contains the key most likely has the capitalisation wrong.
Tested with .NET 10.0.10 and Newtonsoft.Json 13.0.4.
