概要
最近の.NET界隈ではJSONをいじくりまわしたいときはSystem.Text.Jsonを使うのがおすすめのようだ。
たまたま使う機会があったので調べてみたところ、主に以下のことができるみたい。
- JSON文字列のシリアライズ/デシリアライズ
- JsonDocumentによる読み取り専用JSON DOMを使った高速なデータアクセス
- JsonNodeによる読み書き可能なJSON DOMを使ったデータ編集
今回は「3. JsonNodeによる読み書き可能なJSON DOMを使ったデータ編集」についてまとめる。
JsonNodeとJsonDocument
Microsoft DocsのJsonNodeに関する説明は以下のとおりである。
JsonNode
およびその派生クラスを使用すると、変更可能なDOMを作成することができます。
JsonNode
DOMは作成後に変更できます。JsonDocument
DOMは変更できません。
一方JsonDocumentは以下のように説明されている。
JsonDocument
を使用すると、Utf8JsonReader
を使用して読み取り専用DOMを構築することができます。
JsonDocument
DOMでは、そのデータにより高速にアクセスできます。
JSONファイルやJSON文字列をプログラム中に取り込んで解釈したいだけの場合はJsonDocument
を使うとよいが、
JSON内のデータを編集する場合はJsonNode
を使うようだ。
JsonNode
やJsonDocument
はデータ型を動的に決定した場合、すなわち事前に型がわかっていない/型を意識したくない場合に使用するべきだ。
もしJSONデータ内部の型や構造が事前に判明しているのであれば、シリアライズ/デシリアライズを使用したほうが賢明である。
書かないといけないコード量がかなり違うし、たぶん実行速度もシリアライズ/デシリアライズのほうが有利な気がする。
下の姉妹記事も参照してください。
テスト用データ
会社情報(companies
)や社員情報(employees
)からなる謎のデータ。
{
"version": "1.0.0",
"description": "企業情報",
"companies": [
{
"name": "A株式会社",
"address": "東京都○○市",
"employees": [
{
"name": "鈴木",
"age": 21,
"president": false
},
{
"name": "田中",
"age": 30,
"president": false
},
{
"name": "高橋",
"age": 54,
"president": true
}
]
},
{
"name": "株式会社B",
"address": "大阪府××市",
"employees": [
{
"name": "上田",
"age": 43,
"president": true
},
{
"name": "小沢",
"age": 27,
"president": false
}
]
},
{
"name": "C Co., Ltd.",
"address": "愛知県××市",
"employees": [
{
"name": "平井",
"age": 36,
"president": true
}
]
}
]
}
Json文字列を読み込んで分析する
JsonNode
はJSONデータを編集したい場合に使用するべきだが、当然データを読み出すだけの場合も使用できる。
DisplayJsonNodeRecursively()
を再帰的に呼び出してすべての要素の型名と値を表示してみる。
using System;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Unicode;
namespace JsonNodeTest
{
internal class Program
{
internal static string jsonStr =
@"
{
""version"": ""1.0.0"",
""description"": ""企業情報"",
""companies"": [
{
""name"": ""A株式会社"",
""address"": ""東京都○○市"",
""employees"": [
{
""name"": ""鈴木"",
""age"": 21,
""president"": false
},
{
""name"": ""田中"",
""age"": 30,
""president"": false
},
{
""name"": ""高橋"",
""age"": 54,
""president"": true
}
]
},
{
""name"": ""株式会社B"",
""address"": ""大阪府××市"",
""employees"": [
{
""name"": ""上田"",
""age"": 43,
""president"": true
},
{
""name"": ""小沢"",
""age"": 27,
""president"": false
}
]
},
{
""name"": ""C Co., Ltd."",
""address"": ""愛知県××市"",
""employees"": [
{
""name"": ""平井"",
""age"": 36,
""president"": true
}
]
}
]
}
";
static void DisplayJsonNodeRecursively(JsonNode node, int level = 0)
{
// オブジェクトのとき
if (node is JsonObject)
{
string indent = new string('\t', ++level);
foreach (var child in node.AsObject())
{
// オブジェクトの中身(KeyValuePair<string, JsonNode?>型)を取り出す
Console.WriteLine($"{indent}Name: {child.Key}");
Console.WriteLine($"{indent}Path: {child.Value.GetPath()}");
Console.WriteLine($"{indent}Type: {child.Value.GetType().Name}");
// 子のJsonNodeも同様に探索する
DisplayJsonNodeRecursively(child.Value, level);
}
}
// 配列のとき
if (node is JsonArray)
{
string indent = new string('\t', ++level);
foreach (var child in node.AsArray())
{
// オブジェクトの中身(JsonNode?型)を取り出す
Console.WriteLine($"{indent}Path: {child.GetPath()}");
Console.WriteLine($"{indent}Type: {child.GetType().Name}");
// 子のJsonNodeも同様に探索する
DisplayJsonNodeRecursively(child, level);
}
}
// それ以外の型
if (node is JsonValue)
{
string indent = new string('\t', level);
Console.WriteLine($"{indent}Value: {node.AsValue().ToString()}");
}
}
static void Main(string[] args)
{
JsonNode node = JsonNode.Parse(jsonStr);
Console.WriteLine($"Name: root");
Console.WriteLine($"Path: {node.GetPath()}");
Console.WriteLine($"Type: {node.GetType().Name}");
// すべての要素の型名と値を再帰的に表示する
DisplayJsonNodeRecursively(node);
}
}
}
出力
Name: root
Path: $
Type: JsonObject
Name: version
Path: $.version
Type: JsonValueTrimmable`1
Value: 1.0.0
Name: description
Path: $.description
Type: JsonValueTrimmable`1
Value: 企業情報
Name: companies
Path: $.companies
Type: JsonArray
Path: $.companies[0]
Type: JsonObject
Name: name
Path: $.companies[0].name
Type: JsonValueTrimmable`1
Value: A株式会社
Name: address
Path: $.companies[0].address
Type: JsonValueTrimmable`1
Value: 東京都○○市
Name: employees
Path: $.companies[0].employees
Type: JsonArray
Path: $.companies[0].employees[0]
Type: JsonObject
Name: name
Path: $.companies[0].employees[0].name
Type: JsonValueTrimmable`1
Value: 鈴木
Name: age
Path: $.companies[0].employees[0].age
Type: JsonValueTrimmable`1
Value: 21
Name: president
Path: $.companies[0].employees[0].president
Type: JsonValueTrimmable`1
Value: false
Path: $.companies[0].employees[1]
Type: JsonObject
Name: name
Path: $.companies[0].employees[1].name
Type: JsonValueTrimmable`1
Value: 田中
Name: age
Path: $.companies[0].employees[1].age
Type: JsonValueTrimmable`1
Value: 30
Name: president
Path: $.companies[0].employees[1].president
Type: JsonValueTrimmable`1
Value: false
Path: $.companies[0].employees[2]
Type: JsonObject
Name: name
Path: $.companies[0].employees[2].name
Type: JsonValueTrimmable`1
Value: 高橋
Name: age
Path: $.companies[0].employees[2].age
Type: JsonValueTrimmable`1
Value: 54
Name: president
Path: $.companies[0].employees[2].president
Type: JsonValueTrimmable`1
Value: true
Path: $.companies[1]
Type: JsonObject
Name: name
Path: $.companies[1].name
Type: JsonValueTrimmable`1
Value: 株式会社B
Name: address
Path: $.companies[1].address
Type: JsonValueTrimmable`1
Value: 大阪府××市
Name: employees
Path: $.companies[1].employees
Type: JsonArray
Path: $.companies[1].employees[0]
Type: JsonObject
Name: name
Path: $.companies[1].employees[0].name
Type: JsonValueTrimmable`1
Value: 上田
Name: age
Path: $.companies[1].employees[0].age
Type: JsonValueTrimmable`1
Value: 43
Name: president
Path: $.companies[1].employees[0].president
Type: JsonValueTrimmable`1
Value: true
Path: $.companies[1].employees[1]
Type: JsonObject
Name: name
Path: $.companies[1].employees[1].name
Type: JsonValueTrimmable`1
Value: 小沢
Name: age
Path: $.companies[1].employees[1].age
Type: JsonValueTrimmable`1
Value: 27
Name: president
Path: $.companies[1].employees[1].president
Type: JsonValueTrimmable`1
Value: false
Path: $.companies[2]
Type: JsonObject
Name: name
Path: $.companies[2].name
Type: JsonValueTrimmable`1
Value: C Co., Ltd.
Name: address
Path: $.companies[2].address
Type: JsonValueTrimmable`1
Value: 愛知県××市
Name: employees
Path: $.companies[2].employees
Type: JsonArray
Path: $.companies[2].employees[0]
Type: JsonObject
Name: name
Path: $.companies[2].employees[0].name
Type: JsonValueTrimmable`1
Value: 平井
Name: age
Path: $.companies[2].employees[0].age
Type: JsonValueTrimmable`1
Value: 36
Name: president
Path: $.companies[2].employees[0].president
Type: JsonValueTrimmable`1
Value: true
別記事で説明したJsonDocument
を使った方法に比べてJson Pathによるパス情報(GetPath()
)や親情報(Parent
)を容易に取得できるようになっている。
JsonDocument
のValueKind
のような中身のデータを判別する手段が用意されていないのが気になる。
JsonNodeにデータを追加・編集・削除する
社員データに以下の編集を加えてみる。
- 社員ID風の一意の値(GUID)を追加する
- 社員の名前の後に"様"をつける
- 社員の年齢情報を削除する
(省略)
static void Main(string[] args)
{
JsonNode node = JsonNode.Parse(jsonStr);
foreach(JsonObject company in node.AsObject()["companies"].AsArray())
{
foreach(JsonObject employee in company["employees"].AsArray())
{
employee["id"] = Guid.NewGuid().ToString();
employee["name"] = employee["name"] + "様";
employee.Remove("age");
}
}
JsonSerializerOptions options = new JsonSerializerOptions
{
// オプションは他にもいろいろ指定できる
WriteIndented = true,
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
string jsonResult = JsonSerializer.Serialize(node, options);
Console.WriteLine(jsonResult);
}
(省略)
出力
{
"version": "1.0.0",
"description": "企業情報",
"companies": [
{
"name": "A株式会社",
"address": "東京都○○市",
"employees": [
{
"name": "鈴木様",
"president": false,
"id": "e32a8e2c-37a0-4bbb-a66e-13ee8571bb26"
},
{
"name": "田中様",
"president": false,
"id": "44d0f273-40ac-44f9-9117-4b4f442136af"
},
{
"name": "高橋様",
"president": true,
"id": "e0a7b7ad-2258-4305-8234-98e737390643"
}
]
},
{
"name": "株式会社B",
"address": "大阪府××市",
"employees": [
{
"name": "上田様",
"president": true,
"id": "edf6e92a-7c3a-46bf-8ba9-9b542e86db90"
},
{
"name": "小沢様",
"president": false,
"id": "79b1d80e-42a2-433f-816d-8b64bd8007a7"
}
]
},
{
"name": "C Co., Ltd.",
"address": "愛知県××市",
"employees": [
{
"name": "平井様",
"president": true,
"id": "f59b6f1a-196a-4b91-a778-2316e9f6de98"
}
]
}
]
}
JsonNodeのデータをコピーする
ちょっとハマりポイント。どうやら各JsonNode
には親情報Parent
が埋め込まれているため、
異なる親にノードをコピーしたい場合にエラーが発生することがある。
例として「A株式会社」の「鈴木」を「株式会社B」に転職させてみよう。これは問題なくうまくいく。
(省略)
static void Main(string[] args)
{
JsonNode node = JsonNode.Parse(jsonStr);
var suzuki = node.AsObject()["companies"].AsArray()[0]["employees"][0];
var employeesOfA = node.AsObject()["companies"].AsArray()[0]["employees"].AsArray();
var employeesOfB = node.AsObject()["companies"].AsArray()[1]["employees"].AsArray();
// A株式会社から退職して
employeesOfA.Remove(suzuki);
// 株式会社Bに就職する
employeesOfB.Add(suzuki);
JsonSerializerOptions options = new JsonSerializerOptions
{
// オプションは他にもいろいろ指定できる
WriteIndented = true,
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
string jsonResult = JsonSerializer.Serialize(node, options);
Console.WriteLine(jsonResult);
}
ところが、株式会社Bに副業として所属する(株式会社Bに社員情報をコピーする)場合はエラーが発生する。
(省略)
static void Main(string[] args)
{
JsonNode node = JsonNode.Parse(jsonStr);
var suzuki = node.AsObject()["companies"].AsArray()[0]["employees"][0];
var employeesOfB = node.AsObject()["companies"].AsArray()[1]["employees"].AsArray();
// ここでエラー!
// System.InvalidOperationException: 'The node already has a parent.'
employeesOfB.Add(suzuki);
JsonSerializerOptions options = new JsonSerializerOptions
{
// オプションは他にもいろいろ指定できる
WriteIndented = true,
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
string jsonResult = JsonSerializer.Serialize(node, options);
Console.WriteLine(jsonResult);
}
回避策は一旦「鈴木」の情報をシリアライズして新たなオブジェクトとして作り直せばよい。
そのためにJsonNode
に新しくCopy()
の拡張メソッドを追加するとよい。
public static class JsonNodeExtensions
{
// 拡張メソッド
public static T Copy<T>(this T node) where T : JsonNode => node.Deserialize<T>();
}
(中略)
static void Main(string[] args)
{
JsonNode node = JsonNode.Parse(jsonStr);
var suzuki = node.AsObject()["companies"].AsArray()[0]["employees"][0];
var employeesOfB = node.AsObject()["companies"].AsArray()[1]["employees"].AsArray();
// 鈴木をいったんコピーする
employeesOfB.Add(suzuki.Copy());
JsonSerializerOptions options = new JsonSerializerOptions
{
// オプションは他にもいろいろ指定できる
WriteIndented = true,
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
string jsonResult = JsonSerializer.Serialize(node, options);
Console.WriteLine(jsonResult);
}
もっといいやり方があれば教えてください!