背景
C++ で JSON を扱うライブラリはいくつかありますが(picojson, rapidjson, json11, nlohmann json など), 具体的なメッセージ構造(クラス, struct)が決まっているときに復元するのはいろいろ型変換や型チェックのコードを書く必要がありめんどい.
(グラフィックスとか機械学習などの用途では, だいたいメッセージ型が決まっているため)
protobuf, flatbuffers のように, スキーマ定義などから JSON を具体的な静的な型を持つメッセージに復元したい.
たとえば以下のような感じ
struct Message {
std::string name;
int id;
};
std::string str = "....";
Message message;
message = message_from_json(str);
ライブラリ
すでに 4 つ良さげなライブラリがあります. ありがたく使わせていただきましょう.
StaticJSON
C++11
JSON の処理には RapidJSON を使っています. パッケージでは v1.1(2016-8-25) を使っていますが, 最新の RapidJSON master
でも, 少なくともコンパイルはうまくいくのを確認しました.
std::string, int, std::map あたりの基本型であれば, デフォルトでよろしく復元してくれます.
カスタムのクラスの場合は, protoc
, flatc
のように autojsoncxx
でスキーマ定義から python でコードを生成します.
C++ コードから, JSON スキーマ定義を出力することもできます.
enum を扱う
enum 対応しています.
階層構造の struct を扱う.
以下のように, struct にそれぞれ staticjson_init
でマッピングを書けばいけます.
struct BlendShapeTargetJ
{
float weight;
std::string mesh_filename;
void staticjson_init(staticjson::ObjectHandler* h)
{
h->add_property("weight", &weight);
h->add_property("mesh", &mesh_filename);
h->set_flags(staticjson::Flags::DisallowUnknownKey);
}
};
struct BlendShapeJ
{
std::string name;
std::vector<BlendShapeTargetJ> targets;
void staticjson_init(staticjson::ObjectHandler* h)
{
h->add_property("name", &name);
h->add_property("targets", &targets);
h->set_flags(staticjson::Flags::DisallowUnknownKey);
}
};
struct RootJ
{
std::vector<BlendShapeJ> blendshapes;
void staticjson_init(staticjson::ObjectHandler* h)
{
h->add_property("blendshapes", &blendshapes);
h->set_flags(staticjson::Flags::DisallowUnknownKey);
}
};
...
void Parse(const std::string &in_str)
{
RootJ root;
staticjson::ParseStatus res;
if (!staticjson::from_json_string(in_str.c_str(), &root, &res)) {
std::cerr << "Failed to parse JSON.\n";
std::cerr << res.description() << "\n";
}
}
とてもすっきり書けますね!
パースエラーなどは staticjson::ParseStatus
を from_json_string
に渡すことで取得できます.
メッセージ構造体に staticjson_init
を含めたくない(staticjson への依存関係を無くしたい)ときは, non-intrusive にやる方法もあります.
namespace staticjson
{
void init(Date* d, ObjectHandler* h)
{
h->add_property("year", &d->year);
h->add_property("month", &d->month);
h->add_property("day", &d->day);
h->set_flags(Flags::DisallowUnknownKey);
}
}
と, staticjson
に struct/class ごとに init
を定義します.
なぜこれで動くのかというと, まず init
がテンプレートで定義されていて, テンプレートではクラスにある staticjson_init()
を呼ぶようになっています.
したがって init
を書き換える(テンプレート特殊化で上書き)ことで, non-intrusive 化を実現しているわけですね.
メンバーを optional にする
デフォルトでは, JSON にプロパティ(メンバー)がみつからないとパースエラーになります.
void init(Date* d, ObjectHandler* h)
{
h->add_property("year", &d->year, staticjson::Flags::Optional);
h->add_property("month", &d->month);
h->add_property("day", &d->day);
}
と,add_property
の引数で optional にできます(year
がみつからなくてもパースエラーにはしない)
std::optional のサポート
JSON にメンバ(key)がなかったら実体を作らない(デフォルト値で実体を作るのが向いていないケース), というのをやりたいときもあります.
C++17 std::optional に対応していました!
optional_support.hpp
をインクルードすればいけます.
コードはそんなに複雑ではないので, optional-lite などを使うように書き換えれば C++11 でも動かせるでしょう.
jsoncons
C++11. C++ コードに直接メッセージ定義を register して使えます.
json のパースには自前のパーサを使っていますので, 普通の json ライブラリとしても利用できます.
CSV や CBOR(バイナリフォーマット), messagepack も対応していたりと, なかなか機能豊富です.
しかも Boost license.
gcc 4.8(RHEL/CentOS7 default) での std::regex 問題もうまく対応していますね.
C++ プログラムの他の部分で rapidjson など他の json ライブラリを使っている場合は, うまく住み分けできるかどうか考える必要があります.
spotify-json
double-conversion ライブラリを使っていて, 浮動小数点数をより正確に扱えます.
StaticJSON よりいろいろな機能があります. enum なども扱えます.
any 型で opaque な型も扱えますし, encode(C++ 構造体から JSON string)の機能もあります.
明示的な記載はありませんが, Android でもコンパイルできましたので Android でも動作するものと思われます.
ただ, Windows(MSVC)では CMake でランタイムライブラリを強制的に切り替えていますので, 自分のアプリに組み込んだときなどにランタイムライブラリの違いでリンクがうまくいかない時があります.
nlohmann JSON
nlohmann JSON でも構造体へ, 構造体から JSON 変換できました.
optional なものには try_get_to が使えそうです.
v3.9 から, NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE
などマクロで一括定義もありました!
optional
パラメータが optional な場合, 楽にやる方法はありません(std::optional
を直接は使えない)
結局は自前でいろいろやることになります.
struct Settings
{
std::string name;
std::array<float, 3> v{0.0f, 0.0f, 0.0f};
std::optional<int> flag;
};
void to_json(nlohmann::json &j, const Settings &d) {
j = nlohmann::json{{"name", d.name}, {"v", d.v}};
// optional
if (d.flag) {
j["flag"] = d.flag.value();
}
}
void from_json(const nlohmann::json &j, Settings &d) {
j.at("name").get_to(d.name);
j.at("v").get_to(d.v);
// optional
if (j.contains("flag")) {
int flag;
j.at("flag").get_to(flag);
d.flag = flag;
}
}
}
こんな感じでしょうか.
json.hpp は使うのは簡単なぶん, json.hpp はコンパイルが劇重になるのが欠点ですね.
どれを使う?
とりあえずぺろっと使いたい場合は jsoncons,
jsoncons ほどの機能はいらない, rapidjson を使いたい(schema validation や schema 出力をやりたい. jsoncons では schema 機能は無い)などの場合は StaticJSON でしょうか
spotify-json, json.hpp は玄人むけ.
筆者は最近は StaticJSON がお気に入りですね.
todo
-
recursive 可能な std::variant を使って, 自前で JSON -> メッセージ構造体への復元のコードを綺麗にかけないか思いを馳せる.
- => 素直に spotify-json or StaticJSON を使ったほうがいいことがわかりました.