LoginSignup
7
6

More than 1 year has passed since last update.

C++11 or later で JSON 文字列から静的なクラス(or struct)へ値を復元する(StaticJSON, jsoncons, spotify-json, nlohmann json)

Last updated at Posted at 2019-08-24

背景

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::ParseStatusfrom_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 を使ったほうがいいことがわかりました.
7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6