DynamicJson - C# 4.0のdynamicでスムーズにJSONを扱うライブラリ

C#4.0の新機能といったらdynamic。外部から来る型が決まってないデータを取り扱うときは楽かしら。とはいえ、実際に適用出来る範囲はそんなに多くはないようです。例えばXMLをdynamicで扱えたら少し素敵かも、と一瞬思いつつもElementsもDescendantsも出来なくてAttributeの取得出来ないXMLは、実際あんまり便利じゃなかったりする。ただ、ちょうどジャストフィットするものがあります。それは、JSONですよ、JSON。というわけで、dynamicでJSONを扱えるライブラリを書いてみました。ライブラリといっても300行程度のクラス一個です。

使い方は非常にシンプルで直感的。まずは、文字列JSONの読み込みの例を。DynamicJson.Parseメソッド一発です。

// Parse (from JsonString to DynamicJson)
var json = DynamicJson.Parse(@"{""foo"":""json"", ""bar"":100, ""nest"":{ ""foobar"":true } }");
 
var r1 = json.foo; // "json" - dynamic(string)
var r2 = json.bar; // 100 - dynamic(double)
var r3 = json.nest.foobar; // true - dynamic(bool)
var r4 = json["nest"]["foobar"]; // インデクサでのアクセスも可
 
// 定義されてるかチェック
var b1 = json.IsDefined("foo"); // true
var b2 = json.IsDefined("foooo"); // false

Parseしたら、あとはJavaScriptと同じ感じにプロパティ名をドット打つだけで値が取り出せます。dynamicとJSONは相性が良いですね、JavaScriptと全く同じ感覚です。注意点としては、存在しないプロパティ名を読むと例外が出ます。Dictionaryみたいなものだと思ってください。さすがにそれだと使いにくいところもあるので、IsDefinedメソッドであるかないかチェック出来ます。dynamicなため、IntelliSenseには出てこないということは注意してください。もう一つ気をつけなきゃいけないのは、数字は全てdoubleです。JSONでは数値類は全部一緒くたにNumberなので、適宜自分でキャストしてください。

オブジェクトからJSON文字列への変換

dynamicとは関係ないのですが、JSON文字列へのシリアライズも可能です。こちらもDynamicJson.Serializeメソッド一発。

// Serialize (from Object to JsonString)
var obj = new
{
    Name = "Foo",
    Age = 30,
    Address = new
    {
        Country = "Japan",
        City = "Tokyo"
    },
    Like = new[] { "Microsoft", "Xbox" }
};
// {"Name":"Foo","Age":30,"Address":{"Country":"Japan","City":"Tokyo"},"Like":["Microsoft","Xbox"]}
var jsonStringFromObj = DynamicJson.Serialize(obj);

匿名型でサクッとJSONを作り上げられます。非常にお手軽。DataContractJsonSerializerはちょっと大仰すぎなのよねえ、という時にはこれでサクサクッと作ってやってください。匿名型だけじゃなく、普通のオブジェクトでも大丈夫です(その場合はパブリックプロパティからKeyとValueを生成します)。

JSONオブジェクトの再編集・作成

生成したDynamicJsonは可変です。自由に編集して再シリアライズとか出来ます。

var json = DynamicJson.Parse(@"{""foo"":""json"", ""bar"":100, ""nest"":{ ""foobar"":true } }");
 
// 追加、編集、削除が出来ます
json.Arr = new string[] { "NOR", "XOR" }; // Add
json.foo = 5000; // Replace
json.Delete("bar"); // Delete
 
// DynamicJsonから文字列へのシリアライズはToStringを呼ぶだけ
var reJson = json.ToString(); // {"foo":5000,"nest":{"foobar":true},"Arr":["NOR","XOR"]}
 
// 配列はちょっと特殊で、foreachなので扱いたい場合はobject[]にキャストしてください
Console.WriteLine(json.Arr[1]); // XOR
foreach (var item in (object[])json.Arr)
    Console.WriteLine(item); // NOR XOR
 
// 新しく作成することも出来ます
dynamic root = new DynamicJson(); // ルートのコンテナ
root.obj = new { }; // 空のオブジェクトの追加は匿名型をどうぞ
root.obj.str = "aaa";
root.obj.@bool = true; // C#の予約語と被る場合は@を先頭につけるとアクセス出来るよ!
root.array = new[] { 1, 200 }; // 配列の追加
root.obj2 = new { str2 = "bbbb", ar = new object[] { "foobar", null, 100 } }; // オブジェクトの追加と初期化
 
// {"obj":{"str":"aaa","bool":true},"array":[1,200],"obj2":{"str2":"bbbb","ar":["foobar",null,100]}}
Console.WriteLine(root.ToString());

追加は存在しないプロパティ名に直接突っ込めばOK。編集はそのまま上書き。型名とか関係ないので、元から入っているものの型に合わせる必要はありません。削除はDeleteメソッドを呼べば出来ます。配列はちょっと扱いが特殊でして、foreachしたかったりLinqメソッド使いたい場合はobject[]にキャストする必要があります。この辺は仕様です。諸事情によりIEnumerableじゃないんです。ごめんなさい。ちなみにobjectってものなあ、intが欲しいんすよ、っていう時は.Cast()とかでLinqに繋げるといいかもですね。

一から新しいDynamicJsonオブジェクトを作成することも出来ます。普通にnewするだけ。注意点としては、変数はdynamicで受けてください。varで受けても何の嬉しいこともありませんので。あとは、普通にぽこぽこ足すだけ。オブジェクトを作る場合は空の匿名型でやります。決してDynamicJsonを足したりしないでください、がっかりなことになりますので。

実装の裏側

300行のクラス一個、ということで、勿論自前でパーサー書いてるわけがありません。ていうか、その手のは自分で書きたくないんだよね、ソートアルゴリズムとかもそうだけど、こういうのはちゃんと検証されてるものを使うべき。(そしてそもそも、ちゃんとしたのが書けるかというと、書けません……)。で、何を使っているかというと先日の記事でLinq to Jsonとか言ってたように、JsonReaderWriterFactoryを使用しています。

ようするに、ただのJsonReaderWriterFactoryのラッパーです。内部ではJSONの構造をXMLとして保持していて、書き出しの際にJsonReaderWriterFactoryを通しています。ただですね、Readerのほうは使い易くてJsonReaderWriterFactoryお薦め!なのですが、Writerのほうは結構厳しいです。ルールに則って書いたXMLを通すとJSONになる、という仕組みなのですが、ルールに則ってないと即弾かれるということでもあって、かなり面倒くさいです。

// 例えばこんなDynamicJSONは
dynamic root = new DynamicJson();
root.Hoge = "aiueo";
root.Arr = new[] { "A", "BC", "D" };
 
// 内部ではこんなコードに変換されています
new XElement("root", new XAttribute("type", "object"),
    new XElement("Hoge", new XAttribute("type", "string"), "aiueo"),
    new XElement("Arr", new XAttribute("type", "array"),
        new XElement("item", new XAttribute("type", "string"), "A",
        new XElement("item", new XAttribute("type", "string"), "BC",
        new XElement("item", new XAttribute("type", "string"), "D")))));

このXElementを素で書いていくのは地獄でしょう。DynamicJsonはこの変換を自動で行います。dynamicでラッピングすることで、煩わしい部分を完全に包み隠すことができました。ここまで簡略化出来ると、DSLの域です。C#は大変素晴らしいデスネ。いや、マジで。

まとめ

クラス一個なので、csファイルをコピペって使ってもいいですし(その場合は追加でSystem.Runtime.Serializationの参照を)、DLLを参照設定に加えても、どちらでもお好きな方をどうぞ。数あるJSONライブラリの中でも、使いやすさはトップクラスなのではないでしょうか(自画自賛)。いや、これは、単純にdynamicの威力の賜物ですね。これを作るまではdynamicについて割と勘違いしていたところもあったのですが、なんというか、DSL向けだと思います。で、DSL指向で行くなら全部プロパティだけで組まないとダメですねえ。IntelliSenseが動かないのでメソッドを使うのは今ひとつ。そういう意味で、IsDefinedじゃなくて.property? とかって感じに、末尾に?をつけるとかどうかな!とか考えてみたんですが、コンパイル通らないのでダメでした、残念。「.あいうえお」なら行けるので、日本語プログラミングDSLが待たれるところです。嘘。

static void Main()
{
    var publicTL = new WebClient().DownloadString(@"http://twitter.com/statuses/public_timeline.json");
    var statuses = DynamicJson.Parse(publicTL);
    foreach (var status in (dynamic[])statuses)
    {
        Console.WriteLine(status.user.screen_name);
        Console.WriteLine(status.text);
    }
}

最後に例として、Twitterのpublic_timeline.jsonを引っこ抜くコードを。凄まじく簡潔です。C#はどこをどう見てもLightWeightですね、本当にありがとうございました。

Comment (9)

Kunio : (05/19 14:53)

おなじみCodePlexからコード頂きました。
まさか日本人だったとは思いませんでした。
国際的な場でContributionされていて素晴らしいですね。

ヤフオクのjson取得でつかってていて一件。
callback関数名が頭に入っている場合があるから
hogehoge({Json文})
の時に、xxxx()を外してParseしてくれると親切かも知れません。
それは純粋なJSON Parserの仕事じゃないか。

ともかくありがとう。

neuecc : (05/22 03:07)

どうもです。
関数名はなるほど、JSONPですね。
JSONPの状態自体はJSONではないので、対応は微妙ですが、
しかし確かにそのまま読めたらいいなってのはなるほどなところです。

gege : (10/18 10:41)

nugetから利用させていただきました、ありがとうございます。

どうもNullableなint型をParseする際にInvalidCastExceptionが発生するみたいです。
(DynamicJson.csの257行目)

該当行に、
if (elementType.IsGenericType && elementType.GetGenericTypeDefinition() == typeof(Nullable)) return null;

と挿入することで現在こちらでは対応していますが、何か正式に対応していただけたら幸いです。
よろしくお願いします。

gege : (10/18 11:06)

連投すいません。
上記の修正ですが、nullableの場合問答無用でnullを返してるので実用上かなり問題がありましたね(汗;

戻り値はnullではなく
return value == null ? null : Convert.ChangeType(value, Nullable.GetUnderlyingType(elementType));
とするほうがよさそうでした。

neuecc : (10/31 22:34)

情報どうもです。
幾つか問題的な話は見つかっているので、検討します。

sinji : (03/06 15:29)

とても便利なので使わせて頂いてます。ありがとうございます。
1点質問したいことがあります。
JSONのキー名に「-」半角ハイフンがが含まれている場合、C#側でアクセスする場合、エラーとなってしまうでのすが、何か対処法はありますでしょうか?
現在はパース前に該当キー名からハイフンを除去して使用しています。

Amari Jun : (04/16 15:16)

お疲れ様です。DynamicJson楽しく使わせていただいています。
プログラムで使っているときに、JSONツリーの一部のツリーを取り出し、加工してもとに戻す時に、普通に使うとDynamicJSONのメンバーをJSONとして以下のようなツリーを作ります。

{”IsObject”:true,”IsArray”:false}

例えばTrySetメソッドなどで代入値の型が”Codeplex.Data.DynamicJson”だった場合、以下のように内部のxmlツリーを直接継いでしまうとこのようなケースでも上手く使えて嬉しいです。
これは何か仕様的に問題があって回避したのでしょうか。

private bool TrySet(string name, object value)
{
var type = GetJsonType(value);
var element = xml.Element(name);
if (element == null)
{
if (value.GetType().FullName == “Codeplex.Data.DynamicJson”)
{
xml.Add(((DynamicJson)value).xml); // 既にDynamicJsonなので、メンバーを列挙せずそのまま自分のツリーを追加してしまう
}
else
{
xml.Add(new XElement(name, CreateTypeAttr(type), CreateJsonNode(value)));
}
}

むずでょ : (12/14 01:26)

将棋エンジンの思考ログに 使わせてもらっているぜ☆ サンキューなんだぜ☆!

データ用のクラスをわざわざ作らなくていいし、手間が増えずに できることが増えて快調だぜ☆
コンパクトで助かる☆ww

Lion : (09/18 17:05)

最高ですねこれ。ありがたいです。下手にごちゃごちゃせず、単なるクラスファイルなのでCSファイルを単にぶち込むだけっていうお手軽さもいいです。

Name
WebSite(option)
Comment

Trackback(3) | http://neue.cc/2010/04/30_256.html/trackback

ふと思う--ちょっと考える (いたずら編) : (06/24 19:35)

C#でJSONを扱うなら…

Web(HTTP)ベースでデータの受け渡しには、簡素な構造と送られてきたデータの解析のしやすさからJSONが良く使われます。 実際、JavaScriptやRubyでは、送られてきたデータを直接evalことで変数…

C#で設定保存 « sabonya@atlas : (07/06 11:21)

[…] neue cc – DynamicJson – C# 4.0のdynamicでスムーズにJSONを扱うライブラリ ふと思う–ちょっと考える (いたずら編): デジタル・インターネット […]

C#でJSONを扱うなら | やってみようよ! : (07/01 09:27)

[…] このdynamic JSONは、JavaScriptのような感覚でデータ型の事前定義の必要なく手軽に扱えます。また、入れ子状になったデータも、そのまま扱えます。これは、名前の由来である.NET 4.0で導入されたdynamic機能を使っているからです。作者も「数あるJSONライブラリの中でも、使いやすさはトップクラスなのではないで…」と自画自賛しているが、全くその通りです。 […]

Search/Archive

Category

Profile


Yoshifumi Kawai
Microsoft MVP for .NET(C#)

April 2011
|
March 2017

Twitter:@neuecc
GitHub:neuecc
ils@neue.cc