C++のJSONライブラリには、例えばpicojsonとかboost/property_treeとかいくつかのものがあるが、それらのライブラリの中でもnlohmann-jsonライブラリが使いやすさや完成度、使用実績の点で群を抜いている。
多くのプロジェクトで使われている人気のあるライブラリなのだが、日本語の記事があまりないので本家のreadmeから重要な部分を要約する形で紹介する。
githubページはこちら。
https://github.com/nlohmann/json
この記事で使ったソースコード全体のgistはこちらにある。
https://gist.github.com/yohm/27d69509403b593778b2564e61bbc871
特徴
- 直感的なシンタックス
- ヘッダオンリー
- 標準C++11で書かれていて外部ライブラリへの依存無し
- homebrewでもインストールできる
- 高い信頼性
- ユニットテストもしっかり書いてある(カバレッジ100%!)
- macOSやiOSでも使われているらしい(!)
- 豊富な機能
- STLや自前クラスとの変換
- リテラルから初期化しやすい。stringリテラルからJSONへの変換
- インデント幅を指定したpretty-printもできる
- BSON, msgpackなどのバイナリへの変換(!)
- JSON間のdiffを取ったりマージしたりもできる
- 商用利用も可能なオープンソースライセンス
- MITライセンス
多くのプロジェクトで使われているらしくgithub上のstar数17.6Kになっている。
また必要そうな機能はすべて一式入っていて、C++らしいインターフェースになっている。
よほど強いこだわりがなければこれを使っておけばOKでしょう。
使い方
インストール方法
headerファイルをインクルードするだけ。
もしくはhomebrewをつかってインストールすることもできる。
brew tap nlohmann/json
brew install nlohmann-json
jsonクラスの使い方
以後、表記を簡潔にするため次の様なnamespaceを使う。
#include <nlohmann/json.hpp>
// for convenience
using json = nlohmann::json;
JSONのデータをnlohmann::json
クラスのインスタンスに格納して使う。
例えば、以下の様なオブジェクトを作りたい場合
{
"pi": 3.141,
"happy": true,
"name": "Niels",
"nothing": null,
"answer": {
"everything": 42
},
"list": [1, 0, 2],
"object": {
"currency": "USD",
"value": 42.99
}
}
jsonオブジェクトに対して、mapの様にデータを追加していくことができる。
json j;
j["pi"] = 3.141;
j["happy"] = true;
j["name"] = "Niels";
j["nothing"] = nullptr;
j["answer"]["everything"] = 42; // 存在しないキーを指定するとobjectが構築される
j["list"] = { 1, 0, 2 }; // [1,0,2]
j["object"] = { {"currency", "USD"}, {"value", 42.99} }; // {"currentcy": "USD", "value": 42.99}
std::cout << j << std::endl; // coutに渡せば出力できる。
もしくは初期化リストを渡すともっと簡単に初期化できる。
json j2 = {
{"pi", 3.141},
{"happy", true},
{"name", "Niels"},
{"nothing", nullptr},
{"answer", {
{"everything", 42}
}
},
{"list", {1, 0, 2}},
{"object", {
{"currency", "USD"},
{"value", 42.99}
}
}
};
ちなみに空の配列、objectは以下の様にかける。
json empty_list = json::array(); // []
json empty_obj = json::object(); // {}
と書くことができる。
jsonオブジェクトとjson文字列の変換
JSON形式の文字列リテラルからjson
インスタンスを構築することができる。
json j = R"({ "happy": true, "pi": 3.141 })"_json;
ここで_json
という見慣れないサフィックスがあるが、これはC++11で導入されたユーザー定義リテラルというもの。
https://cpprefjp.github.io/lang/cpp11/user_defined_literals.html
これを使うと文字列リテラルにsuffixをつけた時のメソッドを定義でき、このライブラリではjson
クラスのインスタンスに変換するメソッドが定義されている。
ちなみにプレフィックスのR
はC++11で導入された生文字列リテラル。"
などの文字をエスケープする必要がなくなる。
https://cpprefjp.github.io/lang/cpp11/raw_string_literals.html
リテラルじゃない文字列を変換するにはjson::parse
関数を使う。
std::string s = R"({ "happy": true, "pi": 3.141 } )";
json j = json::parse(s);
逆に、json
インスタンスをJSON形式の文字列にシリアライズするにはdump
メソッドを使う。
std::string s = j.dump(); // {\"happy\":true,\"pi\":3.141}
dump
の引数に数字を渡すことでpretty printされた(整形された)文字列を出すことができる。引数の数字はインデントの幅。例えば、j.dump(4);
とすると出力は以下の様なJSON文字列になる。
{
"happy": true,
"pi": 3.141
}
istream, ostreamに渡すと暗黙的にいい感じにserialize,deserializeできる。
例えば、以下のように直感的な書き方ができる。
また最初の例でstd::cout
に渡すとうまくシリアライズされた文字列が出力されたのは、この暗黙的なシリアライズが行われているから。
std::ifstream i("file.json");
json j;
i >> j;
std::ofstream o("pretty.json");
o << std::setw(4) << j << std::endl; // std::setw でインデント幅を指定できる。
STLのようなアクセス
jsonオブジェクトはmap(objectの場合)やvector(arrayの場合)の様に扱うことができる。
Arrayの場合
まずはarrayの様に扱う場合についての例を以下に示す。任意の型を入れられるvectorのように振舞う。
// create an array using push_back
json j;
j.push_back("foo");
j.push_back(1);
j.push_back(true);
// also use emplace_back
j.emplace_back(1.78);
// iterate the array
for (json::iterator it = j.begin(); it != j.end(); ++it) {
std::cout << *it << '\n';
}
// range-based for
for (auto& element : j) {
std::cout << element << '\n';
}
// getter/setter
const auto tmp = j[0].get<std::string>();
j[1] = 42;
bool foo = j.at(2);
// comparison
if( j == "[\"foo\", 42, true, 1.78]"_json ) {
std::cout << "Equal" << std::endl;
} else {
std::cout << "Not equal" << std::endl;
}
// other stuff
j.size(); // 4
j.empty(); // false
j.clear(); // => j == []
要素を取り出すところで少し注意が必要になる。
operator[]
で取り出したあとはまだjsonインスタンスなので数値や文字列などにget<std::string>()
などを用いて変換する必要がある。
この時、文字列のタイプをintegerなどの違う型に変換しようとすると例外が飛ぶ。
const auto tmp = j[0].get<int>(); // stringが格納されているのにintに変換しようとする
// libc++abi.dylib: terminating with uncaught exception of type nlohmann::detail::type_error: [json.exception.type_error.302]
// type must be number, but is string
またat()
で取り出したときの返り値の型とjsonの型が異なっている場合も例外が起きる。
std::string foo = j.at(2); // boolが入っているのにstringに変換しようとする
// libc++abi.dylib: terminating with uncaught exception of type nlohmann::detail::type_error: [json.exception.type_error.302]
// type must be string, but is boolean
要素の型をチェックするにはis_null
, is_boolean
, is_number
, is_object
, is_array
, is_string
メソッドを使う。
json j = R"( ["foo", 1, true, null, []] )"_json;
for(const json& x: j) {
std::string type;
if( x.is_null() ) { type = "null"; }
else if( x.is_boolean() ) { type = "boolean"; }
else if( x.is_number() ) { type = "number"; }
else if( x.is_object() ) { type = "object"; }
else if( x.is_array() ) { type = "array"; }
else if( x.is_string() ) { type = "string"; }
std::cout << type << std::endl;
}
Objectの場合
objectの場合
// create an object
json o;
o["foo"] = 23;
o["bar"] = false;
o["baz"] = 3.141;
// also use emplace
o.emplace("weather", "sunny");
// special iterator member functions for objects
for (json::iterator it = o.begin(); it != o.end(); ++it) {
std::cout << it.key() << " : " << it.value() << "\n";
}
// the same code as range for
for (auto& el : o.items()) {
std::cout << el.key() << " : " << el.value() << "\n";
}
// find an entry
if (o.find("foo") != o.end()) {
// there is an entry with key "foo"
}
// or simpler using count()
int foo_present = o.count("foo"); // 1
int fob_present = o.count("fob"); // 0
// delete an entry
o.erase("foo");
STLコンテナからのjsonインスタンスへの変換
配列のコンテナ(std::array, std::vector, std::deque, std::forward_list, std::list) で、その要素がJSONの値として使えるもの(e.g., integers, floating point numbers, Booleans, string types, or again STL containers described in this section)は、jsonのarrayに変換される。
std::vector<int> c_vector {1, 2, 3, 4};
json j_vec(c_vector);
// [1, 2, 3, 4]
同様に、連想配列 (std::map, std::multimap, std::unordered_map, std::unordered_multimap) で、keyが文字列に変換可能なもの、かつvalueがJSONの値に変換できるもの (数値、文字列、boolean、他のSTL) はJSONのobjectに変換できる。
(ただし、multimapは最初に出てきたkeyの値が使われる。)
std::map<std::string, int> c_map { {"one", 1}, {"two", 2}, {"three", 3} };
json j_map(c_map);
// {"one": 1, "three": 3, "two": 2 }
暗黙的な変換
いくつかの型(文字列、数値、boolean)は暗黙的にJSON型に変換できる。
// strings
std::string s1 = "Hello, world!";
json js = s1;
auto s2 = js.get<std::string>();
任意の型への変換
例えば以下の様なPersonクラスがあったとする。
namespace ns {
// a simple struct to model a person
struct person {
std::string name;
std::string address;
int age;
};
}
ns::person p = {"Ned Flanders", "744 Evergreen Terrace", 60};
ユーザー定義クラスをjsonに暗黙的に変換するためには、以下の様に2つの関数to_json
, from_json
を定義する必要がある。
この2つの関数を定義しておけば、jsonのコンストラクタが呼ばれた時に自動的にto_json
が呼ばれる。
また、get<MY_TYPE>()
get_to(MY_TYPE& arg)
が呼ばれた時に、自動的にfrom_json
が呼ばれる。
namespace ns {
void to_json(json& j, const person& p) {
j = json{{"name", p.name}, {"address", p.address}, {"age", p.age}};
}
void from_json(const json& j, person& p) {
j.at("name").get_to(p.name); // get_to(T& arg) は arg = get<T>() と同じ
j.at("address").get_to(p.address);
j.at("age").get_to(p.age);
}
} // namespace ns
一度、変換関数を書いてしまえば、次の様にpersonインスタンスとjsonインスタンスを直感的に変換することができる。
ns::person p = {"Ned Flanders", "744 Evergreen Terrace", 60};
json j = p; // jsonへの暗黙的変換
std::cout << j << std::endl;
// {"address":"744 Evergreen Terrace","age":60,"name":"Ned Flanders"}
ns::person p2 = j.get<ns::person>(); // json->personへの変換
注意点としては、
- ユーザー定義型と上記の2つの関数は同じnamespaceで定義されていなければならない。(global namespaceでもよい)
- default constructorが(暗黙的でもよいので)定義されていなければならない。
-
from_json
の中ではoperator[]
ではなくてat()
を使うべき。そうしないとkeyが存在しない時の振る舞いが未定義となり、例外処理もできない。
boostなどのサードパーティーライブラリで既存のnamespaceに追加することはしたくない場合はこちらを参照。
default constructorが定義されていない場合でもムーブコンストラクタが定義されていれば同様に変換できる。こちらを参照。
バイナリフォーマット
このライブラリはJSONのライブラリなのに、なんとBSONやMessagePackなどのバイナリフォーマットへの変換までサポートしている。この辺りは他のJSONライブラリにはない特徴だと思う。
MessagePackとの相互変換はto_msgpack
,from_msgpack
関数を使って簡単にできる。
json j = R"({"foo": 1, "bar": "hello", "baz": true} )"_json;
std::vector<std::uint8_t> v_msgpack = json::to_msgpack(j);
json j2 = json::from_msgpack(v_msgpack);
std::cout << j2 << std::endl; // == {"foo": 1, "bar": "hello", "baz": true}