once upon a time,

Iris Tradをビール片手に聞くのが好きなエンジニアが、機械学習やRubyにまつわる話を書きます

pficommonのpfi::text::jsonでシリアライズ/ デシリアライズしてみた

@unnonounoさんが紹介してくださったpficommonjsonライブラリを試してみました。
pficommonのjsonライブラリはboostみたいにjsonと自分で作ったクラスのオブジェクトとのシリアライズ/デシリアライズができるのがとても魅力です。
その一方で、結構ドキュメントには書いてないことが色々あったので、メモしておきます。

いつものようにコードはgithubにあります。

https://github.com/chezou/pficommon_json_test

今回のポイントは以下の通りです。

  1. wafの使い方
  2. 基本的なアクセスの仕方
  3. stringとjsonの相互変換
  4. オブジェクトとのシリアライズ/デシリアライズ

1.wafの使い方

wafはmakeの代わりのビルドシステムで、pythonでビルドします。詳細な使い方はこちらを参考にしてください。
基本的には、wscriptと呼ばれるMakefileのような物をソースコードがあるディレクトリにそれぞれ書きます。
この際、各ディレクトリに置かれたwscriptが再帰的に呼ばれるので、モジュール毎にディレクトリを切り分けている場合は、そのモジュールの必要なライブラリなどを書いておくべきです。

例としてsrcディレクトリにあるwscriptをのせます。

def build(bld):
    bld.program(
        source = 'json-test.cpp',
        target = 'json-test',
        includes = '. ../include',
	lib = 'pficommon_text pficommon_data',
        use = 'JSON TEST'
        )

見れば分かると思いますが、sourceにソースとなるファイル、targetにビルドした後の実行ファイル名、includesにヘッダーファイルの場所を書きます。libにはライブラリ名を列挙します。このとき、複数の場合はスペースで区切ればOKです。

で、できたらこんな感じでビルドします。

./waf configure
./waf

2.基本的なアクセスの仕方

良くあるmapみたいなアクセスをしていって最後にjson_castをします。

cout << json_cast(js["user"]["id"]) << endl;

3.stringとjsonの相互変換

基本的にはstringstreamを経由します。

実際にはこんな感じでします。

#include <pficommon/text/json.h>
#include <sstream>

using namespace pfi::text::json;
using namespace std;

namespace util{
  json string_to_json(string str){
    json js;
    stringstream ss(str);
    ss >> js;
    return js;
  }

  string json_to_string(json js){
    stringstream oss;
    oss << js;
    return oss.str();
  }
}

こんな感じで使います。

json js = util::string_to_json("{\"foo\":true, \"bar\": \"buzz\"}");
string str = util::json_to_string(js);

4.オブジェクトとのシリアライズ/デシリアライズ

ドキュメントにもありますが、自分で作ったクラスのjsonへのシリアライズをしたい場合は、シリアライズしたい変数をMEMBERマクロで登録しておきます。
boostのような感じですね。

class SouvenirDef
{
public:
  pfi::data::optional<std::string> name;
  pfi::data::optional<int> price;
  pfi::data::optional<bool> famous;
  pfi::data::optional<bool> funny;

  template <class Archive>
  void serialize(Archive &ar){
    ar & MEMBER(price) & MEMBER(famous) & MEMBER(name) & MEMBER(funny);
  }
};

シリアライズしたいときは、to_jsonを、デシリアライズをしたい時はvia_json_with_defaultを使うと良いでしょう(json_castでやると不足したkeyがあるとbad_castになってしまうので)。

stringstream ss("{\"name\":null,\"famouse\":true}");

Souvenir sv2;
ss >> via_json_with_default(sv2);
cout << to_json(sv2) << endl;

いくつか注意点・ポイントです。この辺、知っておくと無駄にハマらないかと思います。

  • 基本的な型はpfi::data::optionalを使う。使わないとnullをシリアライズできない。シリアライズしたい型はpfi::data::optional<T>を使う。シリアライズしたい場合は使わない。
  • 配列を表したい時はvector, ハッシュを表したい時はmapを使う。
  • pairとmap<int, int>は使えない。
特に1点目ですが、サンプルコードのSouvenirクラスを見てください。

class Souvenir
{
 public:
  std::string name;
  int price;
  pfi::data::optional&lt;int> max_price;
  bool famous;
  bool funny;

  template &lt;class Archive>
    void serialize(Archive &ar){
    ar & MEMBER(price) & MEMBER(famous) & MEMBER(name) & MEMBER(funny) & MEMBER(max_price);
  }
};

このmax_priceのようにpfi::data::optionalとすると、変数に値が入っていない場合にto_jsonシリアライズするとnullになってくれるのですが、以下のように直接クラスの値を参照しようとすると、値の有無が0/1で取得できる形になってしまいます。

[20120320追記]

optionalで宣言した型を取得する場合は、ポインタのように*演算子でアクセスするようです。
boost::optionalが参考になるそうです。@eiichiroi さんありがとうございます!


  stringstream ss("{\"name\":null,\"famous\":true,\"max_price\":1200}");

  Souvenir sv2;
  ss >> via_json_with_default(sv2);
  //max_priceは型をpfi::data::optional&lt;int>にしているため、1/0の値で格納される
  //アクセスは*(max_price)でアクセス
  cout &lt;&lt; "name:" &lt;&lt; sv2.name &lt;&lt; " price:" &lt;&lt; sv2.price
       &lt;&lt; " max_price:"  &lt;&lt; &lt;strong>*(sv2.max_price)&lt;/strong> &lt;&lt;  " famous:" &lt;&lt; (sv2.famous ? "true" : "false")
       &lt;&lt; " funny:" &lt;&lt; (sv2.funny ? "true" : "false") &lt;&lt; endl;
  //max_priceをjsonにする場合は問題ない
  cout &lt;&lt; to_json(sv2) &lt;&lt; endl;


このため、jsonを入力としてデシリアライズしたい場合はpfi::data::optionalを利用しない変数を、シリアライズしてjsonに出力したい場合はpfi::data::optionalを利用した変数にしないといけません。

きちんと*演算子でアクセスすることで、入出力対象な形で利用できます!

それでは、pficommonで楽しいJSONライフを!