C# – How to use JsonNode to read, write, and modify JSON

When you don’t want to create classes for JSON (de)serialization, one option is to use JsonNode. This allows you work with JSON as a mutable DOM that consists of JsonNode objects (JsonObject, JsonArray, JsonValue). You can use it to read, write, and modify JSON.

Here’s an example. Let’s say you have the following JSON that you want to modify:

{
  "Name": "Jason",
  "Languages": ["C#", "Java", "JavaScript"],
  "Projects": 10
}Code language: JSON / JSON with Comments (json)

Here’s how to use JsonNode to load and modify this JSON string:

using System.Text.Json;
using System.Text.Json.Nodes;

//read as DOM
var jsonNode = JsonNode.Parse(coderJson, jsonNodeOptions);

//modify it
jsonNode["Projects"] = 11;

//convert back to a JSON string 
var jsonOptions = new JsonSerializerOptions() { WriteIndented = true };
var updatedJson = jsonNode.ToJsonString(jsonOptions);

Console.WriteLine(updatedJson);
Code language: C# (cs)

Here’s what this outputs, showing that it changed the value of the Projects property to 11:

{
  "Name": "Jason",
  "Language": "C#",
  "Projects": 11
}
Code language: JSON / JSON with Comments (json)

It should be noted that there are alternatives to using JsonNode when you don’t want to add classes for (de)serialization:

With that said, I’ll now show examples of how to use JsonNode to read, write, and modify JSON.

Note: For brevity, I’m not showing the ‘using System.Text.Json.Nodes’ and ‘using System.Text.Json’ statements in the code below. Please assume that you need to add those to use JsonNode.

Write JSON

You can use JsonNode to write JSON from scratch. This is a nice alternative to having raw JSON strings in the code. You can add values, arrays (via JsonArray), and objects (via JsonObject) using the familiar object initializer syntax:

var coder = new JsonObject()
{
	["Name"] = "Jason",
	["Languages"] = new JsonArray("C#", "JS"),
	["Preferences"] = new JsonObject()
	{
		["IDE"] = "VS",
		["Monitors"] = 2
	}
};

//convert to JSON string
var jsonOptions = new JsonSerializerOptions() { WriteIndented = true };
var coderJson = coder.ToJsonString(jsonOptions);

System.IO.File.WriteAllText(@"C:\temp\coder.json", coderJson);
Code language: C# (cs)

This generates a JSON string and writes to a file. The file has the following the content:

{
  "Name": "Jason",
  "Languages": [
    "C#",
    "JS"
  ],
  "Preferences": {
    "IDE": "VS",
    "Monitors": 2
  }
}Code language: JSON / JSON with Comments (json)

After creating a JsonObject, you can always modify it as I’ll show in the next section.

Note: If this is producing unexpected JSON for you, it’s most likely because JsonNode ignores most settings from JsonSerializerOptions, except for WriteIndented (and custom converters – according to Microsoft documentation). If this is the case for you, and you can’t just modify the JsonNode properties/values, then you may need to use a custom JSON converter instead to get what you want.

Modify JSON

You can use JsonNode to modify existing JSON. Here’s an example. Let’s say you have the following JSON string that you’re getting from somewhere and you want to modify it:

{
  "Name": "Bob"
}Code language: JSON / JSON with Comments (json)

First, load the JSON string with JsonNode.Parse(). Then you can add and modify properties by indexing into the JsonNode object:

var coder = JsonNode.Parse(coderJson);

//Add new properties
coder["YearsOfExperience"] = 10;

//Modify an existing property
coder["Name"] = "Jason";

var jsonOptions = new JsonSerializerOptions() { WriteIndented = true };
Console.WriteLine(coder.ToJsonString(jsonOptions));
Code language: C# (cs)

Note: If the property already exists, this overwrites it.

This outputs the following modified JSON:

{
  "Name": "Jason",
  "YearsOfExperience": 10
}Code language: JSON / JSON with Comments (json)

I’ll show a few more examples of modifying JSON below.

Remove a property

Here’s an example of removing a JSON property:

var coder = JsonNode.Parse(coderJson);

coder.AsObject().Remove("YearsOfExperience");
Code language: C# (cs)

Note: If the property doesn’t exist, this does nothing. It doesn’t throw an exception.

Add to an array

Let’s say you have the following JSON and you want to add a value to the Languages array:

{
  "Name": "Jason",
  "Languages": [
    "C#"
  ]
}Code language: JSON / JSON with Comments (json)

Here’s how to add a value to a JSON array:

var coder = JsonNode.Parse(coderJson);

coder["Languages"].AsArray().Add("JS");

var jsonOptions = new JsonSerializerOptions() { WriteIndented = true };
Console.WriteLine(coder.ToJsonString(jsonOptions));
Code language: C# (cs)

This outputs the following (value that was added to the array is highlighted):

{
  "Name": "Jason",
  "Languages": [
    "C#",
    "JS"
  ]
}
Code language: JSON / JSON with Comments (json)

Read about how you’d deserialize this JSON array to a list.

Add a property without overwriting an existing one

You can use the null-coalescing assignment operator (??=) as a simple way to add a property only if it doesn’t already exist. This is useful when you don’t want to overwrite existing properties. Here’s an example. Let’s say you have the following JSON:

{
  "Name": "Jason"
}Code language: JSON / JSON with Comments (json)

Now let’s say you want to add a property called Projects with a default value of 0, but you don’t want to overwrite it if it already exists. Here’s how to do that with the ??= operator:

var coder = JsonNode.Parse(coderJson);

coder["Projects"] ??= 0;

var currentProjects = (int)coder["Projects"];
Console.WriteLine($"Coder has {currentProjects} ongoing project(s)");
Code language: C# (cs)

This outputs the following, indicating that it added the property:

Coder has 0 ongoing project(s)Code language: plaintext (plaintext)

Now let’s say this JSON string already has the property – “Projects”: 1. This time running the code outputs the following, indicating that it didn’t overwrite the property (otherwise the value would be 0):

Coder has 1 ongoing project(s)Code language: plaintext (plaintext)

Read JSON

While the main purpose of JsonNode is to write and modify JSON, you might need to read values while you’re creating or modifying JSON.

For example, let’s say you have the JSON that you want to read:

{
  "Name": "Jason",
  "Language": "C#",
  "Started": "2022-01-01T00:00:00"
}Code language: JSON / JSON with Comments (json)

Here’s the simplest way to read a property and get its underlying value while guarding against nulls (because JsonNode returns null if the property doesn’t exist):

var coder = JsonNode.Parse(coderJson);

var started = (DateTime?)coder["Started"];

if (started.HasValue)
{
	Console.WriteLine($"Coder started in year {started.Value.Year}");
}
else
{
	Console.WriteLine("Coder hasn't started yet");
}
Code language: C# (cs)

This outputs:

Coder started in year 2022Code language: plaintext (plaintext)

What if the property doesn’t exist? It outputs this (because DateTime? is null):

Coder hasn't started yetCode language: plaintext (plaintext)

Besides guarding against nulls (due to properties not existing), there are two other main problems to watch out for when reading properties, which I’ll show below.

Casting can fail

When you have a type mismatch when trying to get the underlying value, such as trying to cast a number to a DateTime, you’ll get the following exception:

System.InvalidOperationException: An element of type ‘Number’ cannot be converted to a ‘System.DateTime’.

Here’s an example. Let’s say you have the following JSON:

{
  "Name": "Jason",
  "Started": 1
}Code language: JSON / JSON with Comments (json)

When you try to read the Started property as a DateTime, it will throw the InvalidOperationException:

var coder = JsonNode.Parse(coderJson);

var started = (DateTime?)coder["Started"];
Code language: C# (cs)

If this data is necessary for your code to run correctly, I would suggest treating this like a fatal error. In that case, you either need to change the code to use the right type or fix the JSON data.

Otherwise, if the data is optional, you can use TryGetValue() to attempt to safely get the underlying value. You’ll probably want to guard against nulls at the same time. Here’s how to do that (assuming you aren’t interested in distinguishing the type of problem – null or wrong type):

DateTime? started = null;
coder["Started"]?.AsValue().TryGetValue(out started);

if (started.HasValue) 
{ 
	//use value
}
else
{
	Console.WriteLine("Property is missing or isn't a DateTime");
}
Code language: C# (cs)

This outputs the ambiguous error message:

Property is missing or isn't a DateTimeCode language: plaintext (plaintext)

Case sensitivity

By default, JsonNode is case sensitive. You can make it case insensitive with a setting. Here’s an example. Let’s say you have the following JSON with camel-cased property names:

{
  "name": "Jason",
  "favoriteNumber": 7
}Code language: JSON / JSON with Comments (json)

To make JsonNode treat the property names as case insensitive, set JsonNodeOptions.PropertyNameCaseInsensitive to true and pass the options in while parsing:

var jsonNodeOptions = new JsonNodeOptions()
{
	PropertyNameCaseInsensitive = true
};

var coder = JsonNode.Parse(coderJson, jsonNodeOptions);

Console.WriteLine((int?)coder["favoriteNumber"]);
Console.WriteLine((int?)coder["FavoriteNumber"]);
Console.WriteLine((int?)coder["FAVORITENUMBER"]);
Code language: C# (cs)

This outputs the following, indicating that the property names are case insensitive:

7
7
7Code language: plaintext (plaintext)