1. はじめに
前のレクチャーではいつも厳密に型付けされた構造で作業してきた。例えば、シリアライズ/デシリアライズするクラス Person があるとする:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
でも時々、データの構造が事前にわからないことがある。例えば:
- 外部サービス向けのパーサを書いているとき、レスポンスの構造が固定されていない。
- クラス全体を埋める必要はなく、一部の情報だけ取り出したい。
- 動的な条件に基づいてJSONを「その場で」編集したり生成したりする必要がある。
そこで登場するのが「動的構造」 — キーと値のセットとしてJSONツリーを保持するオブジェクトで、事前にC#クラスを定義する必要がない。
なぜ多くの場合Newtonsoft.Jsonを使うのか
.NETでJSONを扱う主要な選択肢は2つある:
- System.Text.Json — Microsoft組み込みのライブラリ(.NET Core 3.0以降で発展中)。
- Newtonsoft.Json (Json.NET) — 人気のライブラリで、JObjectやJArrayのようなクラスを提供している。
このレクチャー執筆時点では、System.Text.Json は同じ使い勝手の JObject/JArray の完全な代替をまだ提供していない。だから、複雑または未知のJSON構造を頻繁に解析・変更する必要がある場合、多くは Newtonsoft.Json を選ぶんだ。
基本的な型: JObject, JArray, JValue と JToken ファミリー
- JToken — すべてのJSONノードの基本型。
- JObject — JSONオブジェクト { ... }、キーと値のペアの集合。
- JArray — 配列 [ ... ]。
- JValue — 単一の値(42、"テキスト"、true、null)。
考え方はシンプル: JSONをパースすると、これらのトークンからなる「ツリー」が得られるので、それを辿って検索、変更、削除、追加ができる。
2. 未知のJSONの読み取り
例えば、次のようなJSONが来ていて、事前にクラスを書きたくない / 書けない場合を考えてみよう:
{
"status": "ok",
"amount": 150.5,
"items": [
{
"name": "book",
"qty": 1
},
{
"name": "pen",
"qty": 3
}
]
}
Newtonsoft.Json を使えば、これをツリーに変換してその場で調べられるよ。
例: JSONを JObject に読み込む
using Newtonsoft.Json.Linq;
string json = @"{
""status"": ""ok"",
""amount"": 150.5,
""items"": [
{ ""name"": ""book"", ""qty"": 1 },
{ ""name"": ""pen"", ""qty"": 3 }
]
}";
// 文字列をパースしてツリーを取得
JObject root = JObject.Parse(json);
// 辞書のようにプロパティを取得
string status = (string)root["status"]; // "ok"
double amount = (double)root["amount"]; // 150.5
// items は配列、つまり JArray
JArray items = (JArray)root["items"];
// 配列をループ
foreach (JObject item in items)
{
string name = (string)item["name"];
int qty = (int)item["qty"];
Console.WriteLine($"商品: {name}, 数量: {qty}");
}
クラス宣言なしで必要な部分だけ素早く取り出せて便利だね。
3. インデクサと動的アクセス
インデクサ:
- オブジェクト用: root["status"], root["items"]
- 配列用: items[0], items[1]
値をすぐに必要な型で得たいときは、(string)、(int)、(bool)、(double) のようにキャストすると、ライブラリが型を自動変換してくれる。
データが存在しない可能性がある場合は注意: 存在しないキーにアクセスすると null が返り、直接キャストすると例外になる。チェック付きのメソッドを使う方が良い:
if (root.TryGetValue("amount", out var token))
{
double amount = token.Value<double>();
// この方法は便利: Value<T>() は直接変換してくれる
}
入れ子のオブジェクトや配列も同様に簡単に読める:
// 2番目の商品を取得
JObject secondItem = (JObject)root["items"][1];
string itemName = (string)secondItem["name"]; // "pen"
dynamicでもできるよ:
dynamic droot = root;
Console.WriteLine(droot.status); // "ok"
でも覚えておいて: dynamic を使うとコンパイラはフィールドアクセスをチェックしない — 実行時にエラーが出る可能性がある。
4. JSONツリーのその場での変更
要素の追加
root["currency"] = "RUB"; // 新しいプロパティを追加
items.Add(new JObject
{
["name"] = "eraser",
["qty"] = 2
});
変更と削除
root["status"] = "done"; // 値を変更
items[0]["qty"] = 5; // 1つ目の商品の数量を増やす
items.RemoveAt(1); // 2つ目の商品を削除
最終的に文字列として保存
string modifiedJson = root.ToString();
// 見やすくするなら root.ToString(Formatting.Indented)
5. ゼロからJSON構造を作る
var person = new JObject
{
["name"] = "Alice",
["age"] = 22,
["languages"] = new JArray { "C#", "Python" },
["isStudent"] = true
};
Console.WriteLine(person.ToString(Newtonsoft.Json.Formatting.Indented));
{
"name": "Alice",
"age": 22,
"languages": [
"C#",
"Python"
],
"isStudent": true
}
外部APIに返すデータの一部だけを返したり、条件に応じてJSONを組み立てるときに便利だよ。
6. JObject から自分のオブジェクトを組み立てる方法
柔軟なJSONを厳密なC#オブジェクトに変換する必要がある場合がある。方法は:
- 通常のデシリアライズ: JsonConvert.DeserializeObject<MyClass>(...) を使う。
- JObject から必要なフィールドを取り出して手動でオブジェクトを組み立てる。
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
// 例えば、こんなJSONが来たとする:
string incoming = @"{ ""name"":""Bob"", ""age"":30, ""extraField"":true }";
JObject j = JObject.Parse(incoming);
// 必要なフィールドだけ手動で組み立てる
var person = new Person
{
Name = (string)j["name"],
Age = (int)j["age"],
// extraField は不要 — 問題なし!
};
7. エラー例と注意点: 典型的な落とし穴
動的JSON構造は柔軟だけど「落とし穴」もある:
- 存在しないキー/要素にアクセスすると null が返る; 値型に直接キャストすると例外になる。
- 期待する型と実際の型が合わない(オブジェクトを期待していたのに値が来たなど)とキャストエラーになる。
- オブジェクトのフィールドを列挙するには Properties() を使うと良い:
foreach (var prop in root.Properties())
{
Console.WriteLine($"フィールド: {prop.Name}, 値: {prop.Value}");
}
- Newtonsoft.Json はLINQ風のクエリ(フィルタリングや検索)と相性が良い:
var expensiveItems = items.Where(obj => (int)obj["qty"] > 2);
foreach (var item in expensiveItems)
Console.WriteLine(item);
8. JObject/JArray を扱うときの典型的なミス
ミスその1: 期待するフィールドがない、または型が違う。 よくあるのは、コードが特定のフィールドが存在すると仮定していて、実際は無いか別の型で来る場合。オブジェクトを期待していたのに数値が来るとキャストで例外になる。使用前に存在と型を確認しよう。
ミスその2: ネストされた構造のフィールドに null チェックをせずにアクセスする。 JSONにネストされたオブジェクトがあるとき、キーが違ったり欠けていたりすると、存在しないフィールドへのアクセスで落ちる。入れ子のノードを読む前に null チェックを入れること。
ミスその3: 値が null のときに値型へキャストする。 キーが存在しても値が null の場合、例えば (int)j["age"] は例外を投げる。Value<T>() を使うとデフォルト値が返る(intなら0、stringならnull など)。
ミスその4: 不必要に動的オブジェクトを多用して明確なモデルを使わないこと。 複雑すぎる構造はC#クラスで表現したほうが安全で読みやすい。構造が本当に不明かかなり変動する場合にだけ動的を使うのが良い。
これで、事前定義モデルが無くても JObject と JArray を使って安全にJSONをパース、変更、作成する方法がわかったはずだ。
GO TO FULL VERSION