C# を使うときに、今までふわっと理解して適当に使っていた Json.NETだけど、一度体系的にしっかり理解してみようと思った。Introductionやそのドキュメントを読んで、サンプルを書いて理解してみよう。
現在では、C# の JSON を操作するライブラリとしては、一択感のある Json.NET だが、大きく分けると2つの機能を有する。
- Json を C#のオブジェクトにシリアライズ、デシリアライズする。
- Json を 手動で書いたり、読んだり、クエリーしたりする。LINQ to JSON という名前で呼ばれている。
の大きく2つに分類される。
シリアライズとデシリアライズ
SerializeObject
と DeserializeObject<T>
が最も頻繁に使うメソッドになります。C# のオブジェクトに対して、JSONをシリアライズしたり、デシリアライズします。
public static void Execute()
{
var product = new Product()
{
Name = "Apple",
ExpiryDate = DateTimeOffset.Parse("2008/12/28"),
Price = 3.99M,
Sizes = new string[] { "Small", "Medium", "Large" }
};
string output = JsonConvert.SerializeObject(product);
Console.WriteLine(output);
var deserializedProduct = JsonConvert.DeserializeObject<Product>(output);
}
{"Name":"Apple","ExpiryDate":"2008-12-28T00:00:00-08:00","Price":3.99,"Sizes":["Small","Medium","Large"]}
おそらく 90% ぐらいのユースケースでは、この文法を知っていると対応できると思います。しかし、たまに対応できないケースがあります。
例えば私は durabletaskにコントリビュートしたときに、次の3つのシリアライズの設定をしています。この TraceContextBase
というオブジェクトはサブクラスがあり、自分の子のオブジェクトのリファレンスを持っています。このオブジェクトをシリアライズして、デシリアライズするためには、どのTraceContextBase のサブクラスがシリアライズされたのかという情報を持つ必要があります。それは、TypeNameHandling という設定で実現されています。具体的には、型名がシリアライズされて保存されます。また、PreserveReferenceHandling によって、参照を持てるようになります。また、ReferenceLoopHandling によって、リファレンスが循環するようなケースも対応されます。それぞれについて解説してみましょう。
static TraceContextBase()
{
CustomJsonSerializerSettings = new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.Objects,
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
};
}
TypeNameHandling
Json にシリアライズを行うときに型情報を追加します。例えば、このようなクラスを作成します。インターフェイスとそのサブクラスがあります。
public interface Node
{
string Name { get; }
void Print();
}
public class NodeLeaf : Node
{
public string Name { get; set; }
public void Print()
{
Console.WriteLine($"I am {this.GetType()}");
}
}
public class NodeComposite : Node
{
public string Name { get; set;}
public void Print()
{
Console.WriteLine($"I am {this.GetType()}");
}
}
このオブジェクトをシリアライズしてみましょう。同じインターフェイスですが実装の型が違います。
var nodes = new List<Node>
{
new NodeLeaf { Name = "I'm file."},
new NodeComposite { Name = "I'm directory."}
};
これをTypeNameHandling
をつけてシリアライズしてみます。
var typeNameList = JsonConvert.SerializeObject(nodes, Formatting.Indented, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Objects
});
Console.WriteLine("typeNameHandling: " + typeNameList);
この箇所はつぎのように表示されます。きれいにシリアライズされています。オブジェクトに $type
が追加されていて、型情報が追加されているのがわかると思います。このシリアライズの設定もTypeNameAssemblyFormatによって実施することが可能です。これをFull
に設定すると、アセンブリ名やバージョン名などを付加することも可能です。
typeNameHandling: [
{
"$type": "JTokenSPike.NodeLeaf, JTokenSPike",
"Name": "I'm file."
},
{
"$type": "JTokenSPike.NodeComposite, JTokenSPike",
"Name": "I'm directory."
}
]
デシリアライズするときも設定を追加します。JsonSerializerSettings
と、TypeNameHandling
を設定しないと、この場合は、Exception になります。型情報を読み取らなければ、Node はインターフェイスなのでデシリアライズできないからです。
var deserializedNodeList = JsonConvert.DeserializeObject<IList<Node>>(typeNameList, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Objects
});
foreach (var node in deserializedNodeList)
{
node.Print();
}
実行結果も思った通りですね。
I am JTokenSPike.NodeLeaf
I am JTokenSPike.NodeComposite
PreserveReferenceHandling
次に、参照を保持したいときにどうしたらいいかを見てみましょう。
public class Directory
{
public string Name { get; set; }
public Directory Parent { get; set; }
public IList<File>? Files { get; set; }
}
public class File
{
public string Name { get; set; }
public Directory Parent { get; set; }
}
Directory と File は参照を持っています。これを普通にシリアライズすると、Exception になります。Directory が File
を持っていて、Fileが、Directoryを持っているからです。
try
{
JsonConvert.SerializeObject(document, Formatting.Indented);
} catch (JsonSerializationException ex)
{
Console.WriteLine("Expected: " + ex.ToString());
}
Expected: Newtonsoft.Json.JsonSerializationException: Self referencing loop detected for property 'Parent' with type 'JTokenSPike.Directory'. Path 'Files[0]'.
at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.CheckForCircularReference(JsonWriter writer, Object value, JsonProperty property, JsonContract contract, JsonContainerContract containerContract, JsonProperty containerProperty)
次のように設定します。
var preserveReferenceAll = JsonConvert.SerializeObject(document, Formatting.Indented, new JsonSerializerSettings {
PreserveReferencesHandling = PreserveReferencesHandling.All
});
Console.WriteLine("All: " + preserveReferenceAll);
$id
が作成され、参照されているケースはリファレンスとして表現されています。
All: {
"$id": "1",
"Name": "My Documents",
"Parent": {
"$id": "2",
"Name": "Root",
"Parent": null,
"Files": null
},
"Files": {
"$id": "3",
"$values": [
{
"$id": "4",
"Name": "Important Legal Document.docs",
"Parent": {
"$ref": "1"
}
}
]
}
}
シリアライズの方法は、All以外にもあります。Object は、オブジェクトしてシリアライズを行います。
var preserveReferenceObject = JsonConvert.SerializeObject(document, Formatting.Indented, new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects
});
Console.WriteLine("Object: " + preserveReferenceObject);
先ほどと異なって、Files 部分の下が Array として表現されています。
Object: {
"$id": "1",
"Name": "My Documents",
"Parent": {
"$id": "2",
"Name": "Root",
"Parent": null,
"Files": null
},
"Files": [
{
"$id": "3",
"Name": "Important Legal Document.docs",
"Parent": {
"$ref": "1"
}
}
]
}
他にも Array というのがあります。実行結果は、Allと同じように見えます。ただし、Allと違って、後で解説するReferenceLoopHandling
を設定してあげないと、正しく動作しません。
var preserveReferenceArrays = JsonConvert.SerializeObject(document, Formatting.Indented, new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Arrays,
ReferenceLoopHandling = ReferenceLoopHandling.Serialize
});
Console.WriteLine("Array: " + preserveReferenceArrays);
Array: {
"Name": "My Documents",
"Parent": {
"Name": "Root",
"Parent": null,
"Files": null
},
"Files": {
"$id": "1",
"$values": [
{
"Name": "Important Legal Document.docs",
"Parent": {
"Name": "My Documents",
"Parent": {
"Name": "Root",
"Parent": null,
"Files": null
},
"Files": {
"$ref": "1"
}
}
}
]
}
}
Object と、Arrayの違いは何なのでしょう?私の推測でしかないのですが、小さいコードを書いてみます。
var files = new List<File>
{
new File {Name = "doc 1"}, new File {Name = "doc 2"}
};
var filesObject = JsonConvert.SerializeObject(files, Formatting.Indented, new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects
});
Console.WriteLine("Object: " + filesObject);
var arrayObject = JsonConvert.SerializeObject(files, Formatting.Indented, new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Arrays
});
Console.WriteLine("Array: " + arrayObject);
List だけを作ってシンプルにしました。実行結果の違いは、配列の中としてオブジェクトが表現されているか、オブジェクトの中のValueとして、配列が表現されているかの違いです。ここからはマニュアルから読み取れないので私の推測ですが、JToken という最上位の概念の下に、JObject, JArray という概念が存在します。それと対比していると考えると、Object としてシリアライズするとは、配列をオブジェクトとして解釈しないので、オブジェクトの配列として認識し、Arrayとしてシリアライズするなら、もし Arrayがあったとしたら、JArrayのオブジェクトつまり、配列を表現できるオブジェクトとしてシリアライズするために下記のような仕様になっているのではないでしょうか?
Object: [
{
"$id": "1",
"Name": "doc 1",
"Parent": null
},
{
"$id": "2",
"Name": "doc 2",
"Parent": null
}
]
Array: {
"$id": "1",
"$values": [
{
"Name": "doc 1",
"Parent": null
},
{
"Name": "doc 2",
"Parent": null
}
]
}
ReferenceLoopHandling
既に出てきていますが、循環参照をどのように解決するかです。この設定を入れないと、永遠にシリアライズし続けてしまいます。
public class Employee
{
public string Name { get; set; }
public Employee Manager { get; set; }
}
ReferenceLoopHandling
の設定付きでシリアライズすると下記のような循環参照もシリアライズ可能です。その際は、PreserveReferenceHandling
と併用します。
var joe = new Employee { Name = "Joe User" };
var mike = new Employee { Name = "Mike Manager" };
joe.Manager = mike;
mike.Manager = mike;
var loopReferenceHandling = JsonConvert.SerializeObject(joe, Formatting.Indented, new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ReferenceLoopHandling = ReferenceLoopHandling.Serialize
});
Console.WriteLine("Loop: " + loopReferenceHandling);
きれいにシリアライズされていますね。
Loop: {
"$id": "1",
"Name": "Joe User",
"Manager": {
"$id": "2",
"Name": "Mike Manager",
"Manager": {
"$ref": "2"
}
}
}
Linq to Json
いくつかのケースで、自分で Json のオブジェクトを作って、フィルタをかけたいケースがあります。その場合は、Linq to Json
という機能を使います。Json のオブジェクトは主要なものには次のものがあります。
JToken - abstract base class
JContainer - abstract base class of JTokens that can contain other JTokens
JArray - represents a JSON array (contains an ordered list of JTokens)
JObject - represents a JSON object (contains a collection of JProperties)
JProperty - represents a JSON property (a name/JToken pair inside a JObject)
JValue - represents a primitive JSON value (string, number, boolean, null)
上記のクラス階層になっていますので、Json をパースすると、上記のオブジェクトのツリーが作られます。サンプルを見てみましょう。Newtonsoft.Json.Linq
namespace を using すると使用できるようになります。JObject.Parse
メソッドを使うと、オブジェクトがパースできるようになります。
JObject o = JObject.Parse(@"{
'CPU': 'Intel',
'Drives': [
'DVD read/writer',
'500 gigabyte hard drive'
]
}");
string cpu = (string)o["CPU"];
Console.WriteLine($"CPU: {cpu}");
string firstDrive = (string)o["Drives"][0];
Console.WriteLine($"First Drive: {firstDrive}");
IList<string> allDrives = o["Drives"].Select(t => (string)t).ToList();
foreach (var drive in allDrives)
{
Console.WriteLine("Drive: " + drive);
}
Arrayの場合、JArrayでパースすることもできます。
string json = @"[
'Small',
'Medium',
'Large'
]";
JArray a = JArray.Parse(json);
Console.WriteLine($"0: {a[0]}, 1: {a[1]}, 2: {a[2]}");
0: Small, 1: Medium, 2: Large
StreamReader の ReadFrom
メソッドを直接読むことも可能です。computer.json
のファイル名で、先ほどの、CPUと同じJsonを格納しています。インテリセンスによると、Async メソッドも用意されていました。
using(StreamReader reader = System.IO.File.OpenText(@"computer.json"))
{
JObject obj = (JObject)JToken.ReadFrom(new JsonTextReader(reader));
Console.WriteLine($"CPU: {obj["CPU"]}");
}
CPU: Intel
Linq を使用する
普通に、JObject を使う次のようなコードになり、煩雑ですね。
string json = @"{
'channel': {
'title': 'James Newton-King',
'link': 'http://james.newtonking.com',
'description': 'James Newton-King\'s blog.',
'item': [
{
'title': 'Json.NET 1.3 + New license + Now on CodePlex',
'description': 'Announcing the release of Json.NET 1.3, the MIT license and the source on CodePlex',
'link': 'http://james.newtonking.com/projects/json-net.aspx',
'categories': [
'Json.NET',
'CodePlex'
]
},
{
'title': 'LINQ to JSON beta',
'description': 'Announcing LINQ to JSON',
'link': 'http://james.newtonking.com/projects/json-net.aspx',
'categories': [
'Json.NET',
'LINQ'
]
}
]
}
}";
JObject rss = JObject.Parse(json);
string rssTitle = (string)rss["channel"]["title"];
// James Newton-King
string itemTitle = (string)rss["channel"]["item"][0]["title"];
// Json.NET 1.3 + New license + Now on CodePlex
JArray categories = (JArray)rss["channel"]["item"][0]["categories"];
// ["Json.NET", "CodePlex"]
IList<string> categoriesText = categories.Select(c => (string)c).ToList();
// Json.NET
// CodePlex
Linq を使った例
上記のものを Linq を使ってクエリーしてみます。 シンプルに Select で、タイトルを抽出してみます。素直に実行できていますね。
JObject rss = JObject.Parse(channel);
var postTitles = rss["channel"]["item"].Select(p => p["title"]);
foreach(var title in postTitles)
{
Console.WriteLine($"Title: {title}");
};
Title: Json.NET 1.3 + New license + Now on CodePlex
Title: LINQ to JSON beta
もう少し複雑なケースで、上記の Json にある、category の部分をクエリーします。二重配列になってしまうので、SelectMany
を使って、flatten します。その後、GroupByを使って、グループ化した値を使って新しいオブジェクトを作ります。GroupBy の第一引数が、グループ化したい対象を表すための Function で、2つ目の Function は、Functionを実行した結果が、各グループのリストとして、3つ目の Function に渡されます。ここでは、その値を使って、Count() でグループに属するメンバーの数を数えています。最後に降順ソートを実施して終了。
var categories = rss["channel"]["item"].SelectMany(p => p["categories"]).Values<string>()
.GroupBy(k => k, v => v, (k, vs) => new { Key = k, Value = vs.Count() }).OrderByDescending(p => p.Value);
foreach(var c in categories) {
Console.WriteLine($"{c.Key}: {c.Value}");
}
これは楽ちんですね。
Json.NET: 2
CodePlex: 1
LINQ: 1
手動で強引にデシリアライズ
本来は、下記の Parse はうまく動作しません。なぜかというと、.NET Ojbect のプロパティと、JSON が一致していないからです。ケースセンシティブだからです。ところが、Parseすると、lower case ではアクセスできるのでそれを利用して強引にデシリアライズを実行しているサンプルです。
public class Shortie
{
public string Original { get; set; }
public string Shortened { get; set; }
public string Short { get; set; }
public ShortieException Error { get; set; }
}
public class ShortieException
{
public int Code { get; set; }
public string ErrorMessage { get; set; }
}
string jsonText = @"{
'short': {
'original': 'http://www.foo.com/',
'short': 'krehqk',
'error': {
'code': 0,
'msg': 'No action taken'
}
}
}";
JObject json = JObject.Parse(jsonText);
Shortie shortie = new Shortie
{
Original = (string)json["short"]["original"],
Short = (string)json["short"]["short"],
Error = new ShortieException
{
Code = (int)json["short"]["error"]["code"],
ErrorMessage = (string)json["short"]["error"]["msg"]
}
};
Console.WriteLine(shortie.Original);
// http://www.foo.com/
Console.WriteLine(shortie.Error.ErrorMessage);
// No action taken
SelectToken
SelectToken を書くと、クエリをダイナミックに定義することも可能です。ちなみに、SeelctTokenは、JsonPathもサポートしていますので、かなり複雑なことが出来そうですが、それはそれで頭が混乱しそうなので、SelectTokenと、Linq の組み合わせで十分な気がしますので、あまり調べないことにします。LinqToJsonの最初のサンプルを使ってクエリしてみましょう。
var link = (string)rss.SelectToken("channel.item[1].link");
Console.WriteLine($"SelectToken: Link: {link}");
SelectToken: Link: http://james.newtonking.com/projects/json-net.aspx
まとめ
これでざっくり知りたかったことはわかったので、このブログは一旦終了です。Json.NET のページに行くと Performance Tips とか SerializationAttributesとか面白そうなところは残っていますが、今回のブログは一旦ここまで。また次のお楽しみにしておきます。