C# JsonNodeの使い方完全ガイド|JSONの読み書き・追加・更新・検索で迷わない実践例

はじめに

C#でJSONを扱うとき、固定のクラスに変換するならJsonSerializer、読み取り専用で高速に参照するならJsonDocumentがよく使われます。一方で、「JSONを読み込んだあとに値を追加したい」「一部だけ更新したい」「配列の中から条件に合う要素を探したい」といった動的な操作では、JsonNodeが便利です。

JsonNodeSystem.Text.Json.Nodes名前空間で提供される、JSONをツリー構造として扱うためのAPIです。JsonObjectJsonArrayJsonValueを組み合わせることで、オブジェクト・配列・値をC#コードから柔軟に読み書きできます。Microsoftのドキュメントでも、JsonNodeと派生クラスは変更可能なDOMを作成できるAPIとして説明されています。Microsoft Learn+1

この記事では、C#のJsonNodeについて、基本的な読み込み、値の取得、追加、更新、削除、検索、整形出力、実践例、よくあるエラーまでまとめて解説します。

1. C#のJsonNodeとは?検索ユーザーが最初に知りたい基本

1-1. JsonNodeはSystem.Text.JsonでJSONを動的に扱うためのAPI

JsonNodeは、C#標準のJSONライブラリであるSystem.Text.Jsonに含まれるAPIです。JSONをオブジェクトとして読み込み、プロパティや配列要素にアクセスしたり、後から値を書き換えたりできます。

たとえば、次のようなJSONがあるとします。

JSON
{
"id": 1,
"name": "Taro",
"active": true
}

これをJsonNodeで読み込むと、次のようにプロパティ名を指定して値を取得できます。

C#
using System.Text.Json.Nodes;

string json = """
{
"id": 1,
"name": "Taro",
"active": true
}
""";

JsonNode? node = JsonNode.Parse(json);

int id = node!["id"]!.GetValue<int>();
string name = node["name"]!.GetValue<string>();
bool active = node["active"]!.GetValue<bool>();

Console.WriteLine($"{id}: {name}, active={active}");

JsonNodeの大きな特徴は、読み取るだけでなく編集できることです。JsonDocumentは読み取り専用のDOMであり、MicrosoftのドキュメントでもJsonNodeは作成後に変更でき、JsonDocumentは変更できない一方で高速にアクセスできると説明されています。Microsoft Learn

1-2. JsonNodeでできること:読み取り・追加・更新・削除・検索

JsonNodeを使うと、主に次のようなJSON操作ができます。

操作内容
読み取りJSON文字列やファイルを読み込み、値を取得する
追加オブジェクトに新しいプロパティを追加する
更新既存のプロパティや配列要素を書き換える
削除プロパティや配列要素を削除する
検索配列やネストしたJSONから条件に合う値を探す
書き出し編集後のJSONを文字列やファイルに出力する

たとえば、既存のJSONにupdatedAtという項目を追加する場合は、次のように書けます。

C#
JsonNode? node = JsonNode.Parse("""
{
"id": 1,
"name": "Taro"
}
""");

node!["updatedAt"] = DateTimeOffset.UtcNow.ToString("O");

Console.WriteLine(node.ToJsonString());

このように、JSONを辞書のような感覚で操作できるのがJsonNodeの魅力です。

1-3. JsonNodeが向いているケースと向いていないケース

JsonNodeが向いているのは、JSONの構造が完全には固定されていないケースや、読み込んだJSONを後から加工したいケースです。

たとえば、次のような場面に向いています。

向いているケース理由
設定ファイルを部分的に更新する必要な項目だけ変更しやすい
APIレスポンスから一部の値だけ取り出すクラス定義なしでアクセスできる
JSONに項目を追加して保存する動的にプロパティを追加できる
配列内の要素を検索して更新するJsonArrayとLINQを組み合わせられる
JSON構造が頻繁に変わる固定クラスに縛られにくい

一方で、次のようなケースでは別のAPIを検討したほうがよいです。

向いていないケース代替候補
JSONの構造が固定されているJsonSerializerでクラスに変換
大量データを高速に読みたいJsonDocumentまたはUtf8JsonReader
型安全に扱いたいDTOクラスとJsonSerializer
ストリーミング処理したいUtf8JsonReader

JsonNodeは柔軟ですが、すべての場面で最速・最適というわけではありません。編集しやすさを重視するならJsonNode、型安全性を重視するならJsonSerializer、パフォーマンスを重視するならJsonDocumentUtf8JsonReaderという考え方が基本です。

1-4. JsonObject・JsonArray・JsonValueの違い

JsonNodeは抽象的な基底クラスのような位置づけで、実際のJSON要素は主に次の3種類で表されます。

表すJSON
JsonObjectJSONオブジェクト{ "name": "Taro" }
JsonArrayJSON配列[1, 2, 3]
JsonValue文字列・数値・真偽値などの値"Taro", 100, true

コードで作成すると、次のようになります。

C#
using System.Text.Json.Nodes;

JsonObject user = new JsonObject
{
["id"] = 1,
["name"] = "Taro",
["active"] = true,
["tags"] = new JsonArray("admin", "editor")
};

Console.WriteLine(user.ToJsonString());

出力例は次のようになります。

JSON
{"id":1,"name":"Taro","active":true,"tags":["admin","editor"]}

JsonObjectはプロパティの集まり、JsonArrayは要素の並び、JsonValueは単一の値と考えると理解しやすいです。

2. C#でJsonNodeを使うための準備

2-1. 必要な.NETバージョンと名前空間

JsonNodeを使うには、System.Text.Json.Nodes名前空間を利用します。JsonNodeを含む変更可能なDOM APIは.NET 6以降で利用されるAPIとしてMicrosoftの移行ドキュメントでも説明されています。Microsoft Learn

一般的には、.NET 6以降のプロジェクトであれば追加パッケージなしで利用できることが多いです。古いターゲットフレームワークを使っている場合は、System.Text.Jsonパッケージの追加やターゲットフレームワークの確認が必要です。

プロジェクトファイルの例です。

XML
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>

2-2. using System.Text.Json.Nodesの追加

C#ファイルの先頭に、次のusingを追加します。

C#
using System.Text.Json.Nodes;

整形出力やオプションを使う場合は、あわせて次の名前空間もよく使います。

C#
using System.Text.Json;

日本語のエスケープ制御を行う場合は、次の名前空間も使用します。

C#
using System.Text.Encodings.Web;

2-3. JsonNodeが見つからない・使えないときの原因

JsonNodeが見つからない場合、よくある原因は次のとおりです。

原因対処法
using System.Text.Json.Nodes;がないusingを追加する
.NETのバージョンが古い.NET 6以降を検討する
System.Text.Jsonパッケージが古いNuGetパッケージを更新する
System.Text.JsonNewtonsoft.Jsonを混同しているJsonNodeSystem.Text.Json.Nodesの型として使う
プロジェクトのターゲットが古い.csprojTargetFrameworkを確認する

特にJObjectに慣れている場合、JsonNodeJObjectは別物です。JObjectNewtonsoft.Jsonの型で、JsonNodeSystem.Text.Jsonの型です。

2-4. サンプルで使うJSONデータ

この記事では、次のJSONをベースにサンプルを紹介します。

JSON
{
"appName": "SampleApp",
"version": "1.0.0",
"settings": {
"theme": "dark",
"language": "ja",
"notifications": true
},
"users": [
{
"id": 1,
"name": "Taro",
"role": "admin",
"active": true
},
{
"id": 2,
"name": "Hanako",
"role": "user",
"active": false
}
]
}

C#の文字列として使う場合は、次のように書けます。

C#
string json = """
{
"appName": "SampleApp",
"version": "1.0.0",
"settings": {
"theme": "dark",
"language": "ja",
"notifications": true
},
"users": [
{
"id": 1,
"name": "Taro",
"role": "admin",
"active": true
},
{
"id": 2,
"name": "Hanako",
"role": "user",
"active": false
}
]
}
""";

3. JsonNodeでJSONを読み込む基本

3-1. JsonNode.Parseで文字列JSONを読み込む

文字列のJSONを読み込むには、JsonNode.Parseを使います。

C#
using System.Text.Json.Nodes;

string json = """
{
"appName": "SampleApp",
"version": "1.0.0"
}
""";

JsonNode? root = JsonNode.Parse(json);

Console.WriteLine(root!["appName"]!.GetValue<string>());

出力結果です。

SampleApp

JsonNode.Parseの戻り値はJsonNode?です。そのため、実際のコードではnullチェックを入れるか、JSONが必ず存在する前提で!を使います。

安全に書くなら、次のようにします。

C#
JsonNode? root = JsonNode.Parse(json);

if (root is null)
{
Console.WriteLine("JSONを読み込めませんでした。");
return;
}

Console.WriteLine(root["appName"]?.GetValue<string>());

3-2. ファイルからJSONを読み込む

JSONファイルから読み込む場合は、File.ReadAllTextで文字列として読み込み、JsonNode.Parseに渡します。

C#
using System.Text.Json.Nodes;

string path = "appsettings.json";
string json = File.ReadAllText(path);

JsonNode? root = JsonNode.Parse(json);

Console.WriteLine(root!["appName"]!.GetValue<string>());

非同期で読み込む場合は、次のように書けます。

C#
string json = await File.ReadAllTextAsync("appsettings.json");
JsonNode? root = JsonNode.Parse(json);

ファイルが存在しない可能性がある場合は、事前に確認します。

C#
string path = "appsettings.json";

if (!File.Exists(path))
{
Console.WriteLine("JSONファイルが存在しません。");
return;
}

string json = File.ReadAllText(path);
JsonNode? root = JsonNode.Parse(json);

3-3. JSONのルート要素をJsonObjectとして扱う

ルート要素がJSONオブジェクトであることが分かっている場合は、AsObject()JsonObjectとして扱えます。

C#
JsonNode? root = JsonNode.Parse(json);
JsonObject obj = root!.AsObject();

string appName = obj["appName"]!.GetValue<string>();

Console.WriteLine(appName);

JsonObjectとして扱うと、プロパティの追加・削除が分かりやすくなります。

C#
obj["environment"] = "production";
obj.Remove("version");

Console.WriteLine(obj.ToJsonString());

ただし、ルートが配列の場合にAsObject()を呼ぶと例外になります。ルートの種類が不明な場合は、型チェックをしてから扱います。

C#
JsonNode? root = JsonNode.Parse(json);

if (root is JsonObject obj)
{
Console.WriteLine(obj["appName"]?.GetValue<string>());
}
else if (root is JsonArray array)
{
Console.WriteLine($"配列の要素数: {array.Count}");
}

3-4. 不正なJSONを読み込んだときの例外処理

不正なJSONをJsonNode.Parseで読み込むと、JsonExceptionが発生します。実運用では、外部ファイルやAPIレスポンスが必ず正しいJSONとは限らないため、例外処理を入れておくと安全です。

C#
using System.Text.Json;
using System.Text.Json.Nodes;

string invalidJson = """
{
"name": "Taro",
}
""";

try
{
JsonNode? root = JsonNode.Parse(invalidJson);
Console.WriteLine(root);
}
catch (JsonException ex)
{
Console.WriteLine("JSONの形式が不正です。");
Console.WriteLine(ex.Message);
}

System.Text.Jsonは標準的なJSON形式に厳格です。たとえば、Newtonsoft.Jsonでは許容されることがある単一引用符や引用符なしのプロパティ名も、System.Text.Jsonでは有効なJSONとして扱われない場合があります。Microsoftの移行ドキュメントでも、System.Text.Jsonは二重引用符で囲まれたプロパティ名と文字列値を要求すると説明されています。Microsoft Learn

4. JsonNodeでJSONの値を取得する方法

4-1. プロパティ名を指定して値を取得する

JsonObjectのプロパティは、インデクサーで取得できます。

C#
JsonNode? root = JsonNode.Parse(json);

JsonNode? appNameNode = root!["appName"];
Console.WriteLine(appNameNode);

出力例です。

SampleApp

この時点のappNameNodeJsonNode?です。文字列として取り出すには、GetValue<string>()を使います。

C#
string appName = root!["appName"]!.GetValue<string>();
Console.WriteLine(appName);

4-2. GetValue<T>()で型を指定して取得する

GetValue<T>()を使うと、JSONの値をC#の型として取得できます。

C#
string appName = root!["appName"]!.GetValue<string>();
string version = root["version"]!.GetValue<string>();

Console.WriteLine(appName);
Console.WriteLine(version);

数値や真偽値も取得できます。

C#
string userJson = """
{
"id": 1,
"name": "Taro",
"active": true
}
""";

JsonNode? user = JsonNode.Parse(userJson);

int id = user!["id"]!.GetValue<int>();
string name = user["name"]!.GetValue<string>();
bool active = user["active"]!.GetValue<bool>();

Console.WriteLine($"{id}, {name}, {active}");

型が合わない場合は例外が発生することがあります。たとえば、JSON上は文字列なのにintとして取得しようとすると失敗します。

C#
string json = """
{
"id": "abc"
}
""";

JsonNode? node = JsonNode.Parse(json);

// これは失敗する可能性がある
int id = node!["id"]!.GetValue<int>();

型が不確かな場合は、まず文字列として取得してからint.TryParseなどで変換する方法が安全です。

C#
string? idText = node!["id"]?.GetValue<string>();

if (int.TryParse(idText, out int id))
{
Console.WriteLine(id);
}
else
{
Console.WriteLine("idを数値に変換できません。");
}

4-3. ネストしたJSONの値を取得する

ネストしたJSONは、インデクサーをつなげてアクセスできます。

C#
string theme = root!["settings"]!["theme"]!.GetValue<string>();
string language = root["settings"]!["language"]!.GetValue<string>();
bool notifications = root["settings"]!["notifications"]!.GetValue<bool>();

Console.WriteLine(theme);
Console.WriteLine(language);
Console.WriteLine(notifications);

安全に取得したい場合は、?を使います。

C#
string? theme = root?["settings"]?["theme"]?.GetValue<string>();

if (theme is not null)
{
Console.WriteLine($"テーマ: {theme}");
}
else
{
Console.WriteLine("themeが見つかりません。");
}

ネストが深いJSONでは、途中のキーが存在しないだけでNullReferenceExceptionになることがあります。JsonNodeを使うときは、存在しないキーへのアクセスを想定して?を使うのが重要です。

4-4. 配列JsonArrayの要素を取得する

JSON配列はJsonArrayとして扱います。

C#
JsonArray users = root!["users"]!.AsArray();

JsonNode? firstUser = users[0];

string name = firstUser!["name"]!.GetValue<string>();
Console.WriteLine(name);

出力例です。

Taro

配列の全要素を処理する場合は、foreachを使います。

C#
JsonArray users = root!["users"]!.AsArray();

foreach (JsonNode? user in users)
{
int id = user!["id"]!.GetValue<int>();
string name = user["name"]!.GetValue<string>();
string role = user["role"]!.GetValue<string>();

Console.WriteLine($"{id}: {name} ({role})");
}

配列の要素数はCountで取得できます。

C#
Console.WriteLine($"ユーザー数: {users.Count}");

4-5. nullや存在しないキーを安全に扱う

JsonNodeでは、存在しないキーにアクセスするとnullが返ることがあります。そのため、次のように書くと例外になる可能性があります。

C#
string value = root!["notExists"]!.GetValue<string>();

安全に書くには、???を使います。

C#
string value = root?["notExists"]?.GetValue<string>() ?? "default";

Console.WriteLine(value);

ネストしたキーでも同じです。

C#
string language = root?["settings"]?["language"]?.GetValue<string>() ?? "ja";

値の存在を明示的に確認するなら、JsonObjectとして扱ってContainsKeyを使う方法もあります。

C#
JsonObject obj = root!.AsObject();

if (obj.ContainsKey("appName"))
{
Console.WriteLine(obj["appName"]!.GetValue<string>());
}

5. JsonNodeでJSONに値を追加する方法

5-1. JsonObjectに新しいプロパティを追加する

JsonObjectにプロパティを追加するには、インデクサーに値を代入します。

C#
JsonNode? root = JsonNode.Parse("""
{
"appName": "SampleApp"
}
""");

root!["version"] = "1.0.0";
root["active"] = true;

Console.WriteLine(root.ToJsonString());

出力例です。

JSON
{"appName":"SampleApp","version":"1.0.0","active":true}

JsonObjectを明示的に使う場合は、次のように書けます。

C#
JsonObject obj = new JsonObject
{
["id"] = 1,
["name"] = "Taro"
};

obj["role"] = "admin";

Console.WriteLine(obj.ToJsonString());

5-2. JsonArrayに要素を追加する

JsonArrayに要素を追加するには、Addを使います。

C#
JsonArray numbers = new JsonArray();

numbers.Add(10);
numbers.Add(20);
numbers.Add(30);

Console.WriteLine(numbers.ToJsonString());

出力例です。

JSON
[10,20,30]

文字列や真偽値も追加できます。

C#
JsonArray tags = new JsonArray();

tags.Add("csharp");
tags.Add("json");
tags.Add("jsonnode");

Console.WriteLine(tags.ToJsonString());

オブジェクトを配列に追加する場合は、JsonObjectを作って追加します。

C#
JsonArray users = new JsonArray();

users.Add(new JsonObject
{
["id"] = 1,
["name"] = "Taro"
});

users.Add(new JsonObject
{
["id"] = 2,
["name"] = "Hanako"
});

Console.WriteLine(users.ToJsonString());

5-3. ネストしたオブジェクトを追加する

ネストしたJSONを作る場合は、JsonObjectJsonArrayを組み合わせます。

C#
JsonObject root = new JsonObject
{
["appName"] = "SampleApp",
["settings"] = new JsonObject
{
["theme"] = "dark",
["language"] = "ja"
},
["users"] = new JsonArray
{
new JsonObject
{
["id"] = 1,
["name"] = "Taro"
}
}
};

Console.WriteLine(root.ToJsonString());

整形して出力すると、構造が分かりやすくなります。

C#
using System.Text.Json;

var options = new JsonSerializerOptions
{
WriteIndented = true
};

Console.WriteLine(root.ToJsonString(options));

5-4. 既存JSONに後から項目を追加する実践例

既存のJSONを読み込んで、新しい項目を追加する例です。

C#
JsonNode? root = JsonNode.Parse(json);

root!["environment"] = "production";
root["updatedAt"] = DateTimeOffset.UtcNow.ToString("O");

root["settings"]!["timezone"] = "Asia/Tokyo";

Console.WriteLine(root.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true
}));

settingsの中にtimezoneを追加しています。既存の構造に対して必要な項目だけを後から追加できるため、設定ファイルの更新やAPIレスポンスの加工に便利です。

ただし、settings自体が存在しない可能性がある場合は、次のように存在確認してから追加します。

C#
JsonNode? root = JsonNode.Parse(json);

if (root!["settings"] is null)
{
root["settings"] = new JsonObject();
}

root["settings"]!["timezone"] = "Asia/Tokyo";

6. JsonNodeでJSONの値を更新・削除する方法

6-1. 既存プロパティの値を更新する

既存のプロパティを更新するには、追加と同じようにインデクサーへ新しい値を代入します。

C#
JsonNode? root = JsonNode.Parse(json);

root!["version"] = "1.1.0";

Console.WriteLine(root["version"]!.GetValue<string>());

値が存在しない場合は新規追加、存在する場合は更新になります。

C#
root!["appName"] = "NewSampleApp";
root["environment"] = "staging";

6-2. ネストした値を更新する

ネストした値も、インデクサーをつなげて更新できます。

C#
JsonNode? root = JsonNode.Parse(json);

root!["settings"]!["theme"] = "light";
root["settings"]!["notifications"] = false;

Console.WriteLine(root.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true
}));

途中のキーが存在しない可能性がある場合は、事前に確認します。

C#
if (root!["settings"] is JsonObject settings)
{
settings["theme"] = "light";
}

この書き方なら、settingsが存在しない場合にも例外を避けられます。

6-3. 配列内の要素を更新する

配列の要素はインデックスで更新できます。

C#
JsonArray users = root!["users"]!.AsArray();

users[0]!["name"] = "Yamada Taro";

Console.WriteLine(users[0]!.ToJsonString());

条件に一致する要素を探して更新する場合は、foreachを使うと分かりやすいです。

C#
JsonArray users = root!["users"]!.AsArray();

foreach (JsonNode? user in users)
{
if (user?["id"]?.GetValue<int>() == 2)
{
user["active"] = true;
user["role"] = "editor";
break;
}
}

配列の中の特定ユーザーだけ更新したい場合によく使うパターンです。

6-4. プロパティを削除する

JsonObjectのプロパティを削除するには、Removeを使います。

C#
JsonObject obj = root!.AsObject();

bool removed = obj.Remove("version");

Console.WriteLine($"削除結果: {removed}");
Console.WriteLine(obj.ToJsonString());

ネストしたプロパティを削除する場合は、対象のオブジェクトを取得してからRemoveします。

C#
if (root!["settings"] is JsonObject settings)
{
settings.Remove("notifications");
}

削除対象が存在しない場合でも、Removeは例外ではなく削除できたかどうかを返すため、比較的安全に使えます。

6-5. 配列から要素を削除する

JsonArrayからインデックスを指定して削除するには、RemoveAtを使います。

C#
JsonArray users = root!["users"]!.AsArray();

users.RemoveAt(0);

Console.WriteLine(users.ToJsonString());

条件に一致する要素を削除する場合は、先に対象のインデックスを探してから削除すると安全です。

C#
JsonArray users = root!["users"]!.AsArray();

int removeIndex = -1;

for (int i = 0; i < users.Count; i++)
{
if (users[i]?["id"]?.GetValue<int>() == 2)
{
removeIndex = i;
break;
}
}

if (removeIndex >= 0)
{
users.RemoveAt(removeIndex);
}

ループ中に直接削除するとインデックスがずれることがあるため、削除対象のインデックスを見つけてから削除するのが分かりやすいです。

7. JsonNodeでJSON内のデータを検索する方法

7-1. キー名でJSONの値を探す

JsonObjectの直下にあるキーを探すだけなら、ContainsKeyやインデクサーで十分です。

C#
JsonObject obj = root!.AsObject();

if (obj.ContainsKey("appName"))
{
string appName = obj["appName"]!.GetValue<string>();
Console.WriteLine(appName);
}

存在しない場合にデフォルト値を使うなら、次のように書けます。

C#
string appName = root?["appName"]?.GetValue<string>() ?? "Unknown";

7-2. 配列内の条件に一致する要素を探す

配列内から条件に一致する要素を探すには、foreachで順番に確認します。

C#
JsonArray users = root!["users"]!.AsArray();

JsonNode? foundUser = null;

foreach (JsonNode? user in users)
{
if (user?["id"]?.GetValue<int>() == 1)
{
foundUser = user;
break;
}
}

if (foundUser is not null)
{
Console.WriteLine(foundUser["name"]!.GetValue<string>());
}
else
{
Console.WriteLine("ユーザーが見つかりません。");
}

roleadminのユーザーを探す場合は、次のように条件を変えます。

C#
foreach (JsonNode? user in users)
{
if (user?["role"]?.GetValue<string>() == "admin")
{
Console.WriteLine(user["name"]!.GetValue<string>());
}
}

7-3. ネストしたJSONを再帰的に検索する

JSON全体から特定のキー名を探したい場合は、再帰的に検索する関数を作ると便利です。

C#
static JsonNode? FindByKey(JsonNode? node, string key)
{
if (node is JsonObject obj)
{
foreach (KeyValuePair<string, JsonNode?> property in obj)
{
if (property.Key == key)
{
return property.Value;
}

JsonNode? found = FindByKey(property.Value, key);

if (found is not null)
{
return found;
}
}
}
else if (node is JsonArray array)
{
foreach (JsonNode? item in array)
{
JsonNode? found = FindByKey(item, key);

if (found is not null)
{
return found;
}
}
}

return null;
}

使い方です。

C#
JsonNode? languageNode = FindByKey(root, "language");

if (languageNode is not null)
{
Console.WriteLine(languageNode.GetValue<string>());
}

この方法なら、JSONのどの階層にあるか分からないキーでも検索できます。

7-4. LINQと組み合わせてJsonArrayを検索する

JsonArrayはLINQと組み合わせることもできます。

C#
using System.Linq;
using System.Text.Json.Nodes;

JsonArray users = root!["users"]!.AsArray();

JsonNode? activeUser = users
.FirstOrDefault(user => user?["active"]?.GetValue<bool>() == true);

if (activeUser is not null)
{
Console.WriteLine(activeUser["name"]!.GetValue<string>());
}

複数件を取得する場合は、Whereを使います。

C#
IEnumerable<JsonNode?> activeUsers = users
.Where(user => user?["active"]?.GetValue<bool>() == true);

foreach (JsonNode? user in activeUsers)
{
Console.WriteLine(user!["name"]!.GetValue<string>());
}

条件が複数ある場合も、通常のLINQと同じように書けます。

C#
JsonNode? adminUser = users.FirstOrDefault(user =>
user?["role"]?.GetValue<string>() == "admin" &&
user?["active"]?.GetValue<bool>() == true
);

7-5. 検索結果がない場合の安全な処理

検索結果がない可能性がある場合は、必ずnullチェックを入れます。

C#
JsonNode? user = users.FirstOrDefault(user =>
user?["id"]?.GetValue<int>() == 999
);

if (user is null)
{
Console.WriteLine("該当するユーザーは存在しません。");
}
else
{
Console.WriteLine(user["name"]!.GetValue<string>());
}

デフォルト値を返すメソッドにしておくのも実践的です。

C#
static string GetUserNameOrDefault(JsonArray users, int id)
{
JsonNode? user = users.FirstOrDefault(user =>
user?["id"]?.GetValue<int>() == id
);

return user?["name"]?.GetValue<string>() ?? "Unknown";
}

JsonNodeは柔軟に扱える反面、型やキーの存在がコンパイル時に保証されません。検索処理では、常に「見つからない場合」を想定しておくことが大切です。

8. JsonNodeでJSONを書き出す・整形する方法

8-1. ToJsonStringでJSON文字列に変換する

JsonNodeをJSON文字列に変換するには、ToJsonStringを使います。MicrosoftのAPIドキュメントでも、ToJsonStringは現在のインスタンスをJSON形式の文字列に変換するメソッドとして説明されています。Microsoft Learn

C#
JsonNode? root = JsonNode.Parse(json);

string output = root!.ToJsonString();

Console.WriteLine(output);

出力例です。

JSON
{"appName":"SampleApp","version":"1.0.0","settings":{"theme":"dark","language":"ja","notifications":true},"users":[{"id":1,"name":"Taro","role":"admin","active":true},{"id":2,"name":"Hanako","role":"user","active":false}]}

デフォルトでは改行やインデントがないコンパクトなJSONになります。

8-2. WriteIndentedで見やすく整形する

見やすく整形して出力するには、JsonSerializerOptionsWriteIndentedtrueにします。

C#
using System.Text.Json;

var options = new JsonSerializerOptions
{
WriteIndented = true
};

string output = root!.ToJsonString(options);

Console.WriteLine(output);

出力例です。

JSON
{
"appName": "SampleApp",
"version": "1.0.0",
"settings": {
"theme": "dark",
"language": "ja",
"notifications": true
},
"users": [
{
"id": 1,
"name": "Taro",
"role": "admin",
"active": true
},
{
"id": 2,
"name": "Hanako",
"role": "user",
"active": false
}
]
}

ログ出力や設定ファイルの保存では、WriteIndented = trueにすると人間が読みやすくなります。

8-3. ファイルにJSONを書き出す

編集したJSONをファイルに保存するには、File.WriteAllTextを使います。

C#
var options = new JsonSerializerOptions
{
WriteIndented = true
};

string output = root!.ToJsonString(options);

File.WriteAllText("appsettings.updated.json", output);

非同期で保存する場合は、次のように書けます。

C#
await File.WriteAllTextAsync("appsettings.updated.json", output);

設定ファイルを読み込んで更新し、同じファイルに上書き保存する例です。

C#
string path = "appsettings.json";

string json = await File.ReadAllTextAsync(path);
JsonNode? root = JsonNode.Parse(json);

root!["updatedAt"] = DateTimeOffset.UtcNow.ToString("O");

string output = root.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true
});

await File.WriteAllTextAsync(path, output);

8-4. 日本語や特殊文字を含むJSONの出力

日本語を含むJSONを扱う場合、出力時に文字がエスケープされることがあります。

C#
JsonObject obj = new JsonObject
{
["message"] = "こんにちは",
["html"] = "<div>test</div>"
};

Console.WriteLine(obj.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true
}));

日本語などのエスケープを抑えたい場合は、JavaScriptEncoder.UnsafeRelaxedJsonEscapingを指定できます。

C#
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;

JsonObject obj = new JsonObject
{
["message"] = "こんにちは",
["html"] = "<div>test</div>"
};

var options = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

Console.WriteLine(obj.ToJsonString(options));

ただし、UnsafeRelaxedJsonEscapingは名前のとおり注意が必要です。Microsoftのドキュメントでも、既定のエンコーダーよりエスケープに寛容で、HTMLに影響する文字をエスケープしないため、UTF-8のJSONとして解釈されることが分かっている場合に使うべきと説明されています。Microsoft Learn+1

9. JsonNodeの実践例:よくあるJSON操作パターン

9-1. 設定ファイルのJSONを読み書きする

設定ファイルを読み込み、一部の設定だけ変更して保存する例です。

C#
using System.Text.Json;
using System.Text.Json.Nodes;

string path = "config.json";

string json = await File.ReadAllTextAsync(path);
JsonNode? config = JsonNode.Parse(json);

if (config is null)
{
Console.WriteLine("設定ファイルを読み込めませんでした。");
return;
}

config["settings"]!["theme"] = "light";
config["settings"]!["notifications"] = true;
config["updatedAt"] = DateTimeOffset.UtcNow.ToString("O");

string output = config.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true
});

await File.WriteAllTextAsync(path, output);

設定ファイルのように「全体をクラス化するほどではないが、一部だけ更新したい」場合、JsonNodeは非常に使いやすいです。

9-2. APIレスポンスから必要な値だけ取得する

APIレスポンスから必要な値だけ取得する例です。

C#
string responseJson = """
{
"status": "ok",
"data": {
"user": {
"id": 10,
"name": "Suzuki",
"email": "suzuki@example.com"
}
}
}
""";

JsonNode? response = JsonNode.Parse(responseJson);

int id = response!["data"]!["user"]!["id"]!.GetValue<int>();
string name = response["data"]!["user"]!["name"]!.GetValue<string>();

Console.WriteLine($"{id}: {name}");

安全に取得するなら、次のように書きます。

C#
string? email = response?["data"]?["user"]?["email"]?.GetValue<string>();

if (email is not null)
{
Console.WriteLine(email);
}

APIレスポンス全体をDTOに変換する必要がない場合、JsonNodeで必要な箇所だけ取り出すと手軽です。

9-3. JSON配列にデータを追加して保存する

JSON配列に新しいデータを追加して保存する例です。

C#
string json = """
{
"users": [
{
"id": 1,
"name": "Taro"
}
]
}
""";

JsonNode? root = JsonNode.Parse(json);
JsonArray users = root!["users"]!.AsArray();

users.Add(new JsonObject
{
["id"] = 2,
["name"] = "Hanako"
});

string output = root.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true
});

Console.WriteLine(output);

出力例です。

JSON
{
"users": [
{
"id": 1,
"name": "Taro"
},
{
"id": 2,
"name": "Hanako"
}
]
}

9-4. 条件に一致するデータだけ更新する

ユーザー配列の中からidが一致するデータだけ更新する例です。

C#
JsonNode? root = JsonNode.Parse(json);
JsonArray users = root!["users"]!.AsArray();

int targetId = 2;

foreach (JsonNode? user in users)
{
if (user?["id"]?.GetValue<int>() == targetId)
{
user["active"] = true;
user["role"] = "editor";
break;
}
}

複数のデータを更新する場合は、breakを外します。

C#
foreach (JsonNode? user in users)
{
if (user?["active"]?.GetValue<bool>() == false)
{
user["active"] = true;
}
}

このように、JsonArrayと条件分岐を組み合わせることで、JSON内の特定データだけを柔軟に更新できます。

9-5. JSONを加工して別形式のデータに変換する

JsonNodeで読み込んだJSONを、CSVのような別形式に変換することもできます。

C#
JsonNode? root = JsonNode.Parse(json);
JsonArray users = root!["users"]!.AsArray();

List<string> lines = new List<string>
{
"id,name,role,active"
};

foreach (JsonNode? user in users)
{
int id = user!["id"]!.GetValue<int>();
string name = user["name"]!.GetValue<string>();
string role = user["role"]!.GetValue<string>();
bool active = user["active"]!.GetValue<bool>();

lines.Add($"{id},{name},{role},{active}");
}

string csv = string.Join(Environment.NewLine, lines);

Console.WriteLine(csv);

出力例です。

csv
id,name,role,active
1,Taro,admin,True
2,Hanako,user,False

より本格的なCSV出力では、カンマやダブルクォートのエスケープ処理も必要ですが、JSONから必要な値を取り出して別形式に変換する流れはこのように実装できます。

10. JsonNodeと他のJSON APIの違い

10-1. JsonNodeとJsonDocument・JsonElementの違い

JsonNodeJsonDocumentは、どちらもJSONをDOMとして扱うためのAPIです。ただし、目的が異なります。

API特徴向いている用途
JsonNode変更可能なDOMJSONの追加・更新・削除
JsonDocument読み取り専用DOM高速な参照
JsonElementJsonDocument内の要素値の読み取り

Microsoftのドキュメントでは、JsonNode DOMは作成後に変更でき、JsonDocument DOMは変更できないと説明されています。また、JsonDocumentはデータへのアクセスがより高速であるとも説明されています。Microsoft Learn

つまり、判断基準は次のとおりです。

JSONを編集したい       → JsonNode
JSONを高速に読みたい → JsonDocument
JSONを型に変換したい → JsonSerializer

10-2. JsonNodeとJsonSerializerの使い分け

JsonSerializerは、JSONとC#のクラスを相互変換するためのAPIです。

C#
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
C#
string json = """
{
"id": 1,
"name": "Taro"
}
""";

User? user = JsonSerializer.Deserialize<User>(json);

構造が決まっているJSONなら、この方法が型安全で扱いやすいです。

一方、JsonNodeはクラスを作らずに動的に扱えます。

C#
JsonNode? node = JsonNode.Parse(json);
string name = node!["name"]!.GetValue<string>();

使い分けは次のように考えるとよいです。

状況おすすめ
JSON構造が固定JsonSerializer
JSON構造が可変JsonNode
一部だけ読みたいJsonNodeまたはJsonDocument
型安全に処理したいJsonSerializer
読み込んだJSONを編集したいJsonNode

また、JsonNodeJsonSerializerは組み合わせて使うこともできます。たとえば、一部だけJsonNodeで加工してからクラスに変換できます。

C#
JsonNode? node = JsonNode.Parse(json);

node!["name"] = "Updated Taro";

User? user = node.Deserialize<User>();

10-3. JsonNodeとNewtonsoft.JsonのJObjectの違い

Newtonsoft.Jsonを使っていた人にとって、JsonNodeJObjectに近い感覚で使えます。ただし、完全に同じではありません。

比較JsonNodeJObject
ライブラリSystem.Text.JsonNewtonsoft.Json
標準性.NET標準ライブラリ寄り外部ライブラリ
動的編集可能可能
柔軟性標準JSONに比較的厳格柔軟な挙動が多い
既存資産新しめのコード向き既存プロジェクトで多い

Newtonsoft.JsonJObjectは長く使われてきたため、機能が豊富で柔軟です。一方、JsonNodeSystem.Text.Jsonの一部として使えるため、新しい.NETプロジェクトでは採用しやすい選択肢です。

ただし、Newtonsoft.Jsonから単純に置き換えれば必ず同じ挙動になるわけではありません。たとえば、JSONの許容形式やシリアライズの細かい挙動に差があります。移行時は、既存のJSON形式や例外処理を確認しながら進めることが重要です。Microsoft Learn

10-4. パフォーマンス重視ならどれを選ぶべきか

パフォーマンスを重視する場合、JsonNodeは常に最適とは限りません。JSONをツリーとして作成し、編集しやすくする分、単純な読み取りではJsonDocumentUtf8JsonReaderのほうが向いている場合があります。

目安は次のとおりです。

優先するもの選ぶAPI
編集しやすさJsonNode
型安全性JsonSerializer
読み取り速度JsonDocument
低レベルで高速な読み取りUtf8JsonReader
既存のNewtonsoft資産JObject

特に、大きなJSONを大量に処理する場合は、安易にJsonNodeで全体を読み込むのではなく、必要な範囲だけ処理できる設計を検討するとよいです。

11. JsonNodeでよくあるエラーと対処法

11-1. NullReferenceExceptionが発生する原因

NullReferenceExceptionの多くは、存在しないキーに対して!を付けてアクセスしていることが原因です。

C#
string value = root!["settings"]!["notExists"]!.GetValue<string>();

notExistsが存在しない場合、このコードは失敗します。

対処法は、?を使って安全にアクセスすることです。

C#
string value = root?["settings"]?["notExists"]?.GetValue<string>() ?? "default";

または、事前に存在確認します。

C#
if (root?["settings"] is JsonObject settings &&
settings.ContainsKey("notExists"))
{
Console.WriteLine(settings["notExists"]!.GetValue<string>());
}

JsonNodeでは、キーが存在する前提で書きすぎないことが重要です。

11-2. InvalidOperationExceptionが発生する原因

InvalidOperationExceptionは、ノードの型が想定と違う場合に発生することがあります。

たとえば、配列ではないのにAsArray()を呼ぶケースです。

C#
JsonArray array = root!["settings"]!.AsArray();

settingsがオブジェクトであれば、これは不正です。

対処法は、型チェックしてから扱うことです。

C#
if (root!["users"] is JsonArray users)
{
Console.WriteLine(users.Count);
}
else
{
Console.WriteLine("usersは配列ではありません。");
}

AsObject()AsArray()AsValue()を使うときは、対象のJSON構造が本当にその型かを確認しましょう。

11-3. 型変換に失敗する原因

GetValue<T>()で指定した型と実際のJSONの型が合わない場合、変換に失敗します。

C#
string json = """
{
"count": "10"
}
""";

JsonNode? node = JsonNode.Parse(json);

// JSON上は文字列なので、intとして取得すると失敗する可能性がある
int count = node!["count"]!.GetValue<int>();

この場合は、文字列として取得してから変換します。

C#
string? countText = node!["count"]?.GetValue<string>();

if (int.TryParse(countText, out int count))
{
Console.WriteLine(count);
}

APIレスポンスでは、数値が文字列として返ってくることもあります。型が不明な場合は、まずJSONの実際の形式を確認しましょう。

11-4. 同じJsonNodeを複数箇所に追加できない問題

JsonNodeでは、同じノードを複数の場所にそのまま追加しようとすると問題になることがあります。JsonNodeには親子関係があるため、同じインスタンスを別の場所に使い回すのではなく、必要に応じて複製します。

問題が起きやすい例です。

C#
JsonObject address = new JsonObject
{
["city"] = "Tokyo"
};

JsonObject user1 = new JsonObject
{
["name"] = "Taro",
["address"] = address
};

JsonObject user2 = new JsonObject
{
["name"] = "Hanako",
["address"] = address
};

このように同じaddressノードを複数箇所に追加しようとすると、ノードの親子関係の問題が発生します。

対処法は、DeepClone()を使って複製することです。

C#
JsonObject address = new JsonObject
{
["city"] = "Tokyo"
};

JsonObject user1 = new JsonObject
{
["name"] = "Taro",
["address"] = address
};

JsonObject user2 = new JsonObject
{
["name"] = "Hanako",
["address"] = address.DeepClone()
};

11-5. DeepCloneが必要になるケース

DeepClone()は、JsonNodeを子ノードごと再帰的に複製するメソッドです。MicrosoftのAPIドキュメントでも、DeepClone()JsonNodeの新しいインスタンスを作成し、すべての子ノードを再帰的に複製すると説明されています。Microsoft Learn

DeepClone()が必要になる主なケースは次のとおりです。

ケース理由
同じテンプレートJSONを複数箇所に追加したい同じノードを使い回せないため
元のJSONを残して加工したい破壊的変更を避けるため
更新前と更新後を比較したい元データのコピーが必要なため
配列に同じ構造のオブジェクトを複数追加したい個別のノードとして追加するため

例です。

C#
JsonObject template = new JsonObject
{
["role"] = "user",
["active"] = true
};

JsonArray users = new JsonArray();

JsonObject user1 = template.DeepClone().AsObject();
user1["id"] = 1;
user1["name"] = "Taro";

JsonObject user2 = template.DeepClone().AsObject();
user2["id"] = 2;
user2["name"] = "Hanako";

users.Add(user1);
users.Add(user2);

Console.WriteLine(users.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true
}));

テンプレートを使って複数のJSONオブジェクトを作る場合は、DeepClone()を使うと安全です。

12. C# JsonNodeのFAQ

12-1. JsonNodeはいつから使えますか?

JsonNodeSystem.Text.Json.Nodes名前空間で提供されるAPIです。Microsoftのドキュメントでは、JsonNodeおよび派生クラスを使うことで変更可能なDOMを作成できると説明されています。Microsoft Learn+1

実務では、.NET 6以降のプロジェクトで使うのが分かりやすいです。古い.NET Frameworkや古い.NET Coreプロジェクトで使う場合は、ターゲットフレームワークとSystem.Text.Jsonパッケージの対応状況を確認してください。

12-2. JsonNodeでJSONを動的に編集できますか?

できます。JsonNodeは変更可能なDOMとして使えるため、プロパティの追加、値の更新、削除、配列要素の追加・削除が可能です。

C#
JsonNode? node = JsonNode.Parse("""
{
"name": "Taro"
}
""");

node!["age"] = 30;
node["name"] = "Yamada Taro";

Console.WriteLine(node.ToJsonString());

出力例です。

JSON
{"name":"Yamada Taro","age":30}

12-3. JsonNodeで配列を検索できますか?

できます。JsonArrayを使って、foreachやLINQで検索できます。

C#
JsonArray users = root!["users"]!.AsArray();

JsonNode? user = users.FirstOrDefault(user =>
user?["id"]?.GetValue<int>() == 1
);

if (user is not null)
{
Console.WriteLine(user["name"]!.GetValue<string>());
}

配列の中にオブジェクトが並んでいるJSONでは、JsonArrayとLINQの組み合わせが便利です。

12-4. JsonNodeはNewtonsoft.Jsonの代わりになりますか?

一部の用途では代わりになります。JObjectで行っていたような、JSONの読み込み、値の取得、追加、更新、削除といった操作はJsonNodeでも可能です。

ただし、完全互換ではありません。Newtonsoft.Jsonの柔軟なパース挙動や独自機能に依存している場合は、移行時に差分を確認する必要があります。Microsoftの移行ドキュメントでも、Newtonsoft.JsonSystem.Text.Jsonでは受け入れるJSON形式などに違いがあることが説明されています。Microsoft Learn

新規プロジェクトで標準ライブラリ寄りにしたい場合はJsonNode、既存コードがJObjectに強く依存している場合は段階的な移行を検討するとよいです。

12-5. JsonNodeとJsonSerializerは一緒に使えますか?

使えます。JsonNodeで一部を加工してから、JsonSerializerでクラスに変換できます。

C#
using System.Text.Json;
using System.Text.Json.Nodes;

public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
C#
JsonNode? node = JsonNode.Parse("""
{
"id": 1,
"name": "Taro"
}
""");

node!["name"] = "Yamada Taro";

User? user = node.Deserialize<User>();

Console.WriteLine(user?.Name);

逆に、クラスをJsonNodeに変換することもできます。

C#
User user = new User
{
Id = 1,
Name = "Taro"
};

JsonNode? node = JsonSerializer.SerializeToNode(user);

node!["name"] = "Updated Taro";

Console.WriteLine(node.ToJsonString());

型安全な処理はJsonSerializer、動的な加工はJsonNodeというように組み合わせると、実用的なJSON処理が書きやすくなります。

まとめ

C#のJsonNodeは、JSONを動的に読み書きしたいときに便利なAPIです。JsonObjectJsonArrayJsonValueを使うことで、JSONオブジェクト、配列、値を直感的に扱えます。

特に、次のような操作ではJsonNodeが役立ちます。

操作JsonNodeでの方法
JSONを読み込むJsonNode.Parse
値を取得するnode["key"]?.GetValue<T>()
オブジェクトに追加するnode["newKey"] = value
値を更新するnode["key"] = newValue
プロパティを削除するJsonObject.Remove
配列に追加するJsonArray.Add
配列から削除するJsonArray.RemoveAt
JSONを出力するToJsonString
整形するWriteIndented = true
複製するDeepClone()

一方で、JSONの構造が固定されている場合はJsonSerializer、読み取り性能を重視する場合はJsonDocument、低レベルで高速な処理が必要な場合はUtf8JsonReaderも候補になります。

JsonNodeは、JSONを「読み込むだけ」ではなく「追加・更新・削除・検索して加工する」場面で真価を発揮します。C#でJSONを柔軟に扱いたい場合は、まずJsonNode.ParseGetValue<T>()JsonObjectJsonArrayToJsonStringの基本操作から押さえておくと、実務でも迷わず使えるようになります。