本稿のコード例はC++20で示しています。
概要
nlohmann/json
single header
が提供されています。"single"と言っても前方宣言用のヘッダファイルjson_fwd.hpp
もあります。
特徴
- UIが優れている
- 速度とメモリはあまりこだわっていない
- 悪いわけではない
すっごい便利です。
あまりにも便利すぎるので神クラスを作りかねない
コンパイル/ビルド
ディレクトリ例:
workspace/ |--include/ | |--nlohmann/ | |--json_fwd.hpp | |--json.hpp | |--source/ |--main.cpp
main.cpp:
#include <nlohmann/json_fwd.hpp> // なくても動く #include <nlohmann/json.hpp> int main(){}
コンパイル(GCC)
-I Path/To/File
でヘッダファイルへのパスを指定する。
{workspace}$ g++ source/main.cpp -o main -I ./include
データ格納
#include <nlohmann/json.hpp> #include <fstream> using json = nlohmann::json; // 推奨されているエイリアス int main() { const json j1 = {"key", "value"}; const json j2 = {"array", {3, 1, 4}}; const json j3 = R"({"key":"value"})"_json; const json j4 = json::parse(R"({"key":"value"})"); const json j5 = json::parse(std::ifstream("example.json")); const json j6 = 42; const json j7 = "string"; }
文字列からの構築はママだが、オブジェクトからの構築は少しややこしい。
次の2つを念頭に置くと理解しやすいかも。
const std::vector<int> a(3); // [0, 0, 0] const std::vector<int> b{3}; // [3]
// C++11 初期化子リスト // https://cpprefjp.github.io/lang/cpp11/initializer_lists.html const std::vector<json> v1 = {{json{1}, json{2}, json{3}}}; // C++14 ネストする集成体初期化における波カッコ省略を許可 // https://cpprefjp.github.io/lang/cpp14/brace_elision_in_array_temporary_initialization.html const std::vector<json> v2 = {json{1}, json{2}, json{3}}; // C++17 クラステンプレートのテンプレート引数推論 // https://cpprefjp.github.io/lang/cpp17/type_deduction_for_class_templates.html const std::vector v3 = {json{1}, json{2}, json{3}}; // 暗黙の型変換 int -> json const std::vector<json> v4 = {1, 2, 3};
以上を踏まえてこうなる:
#include <nlohmann/json.hpp> #include <iostream> using json = nlohmann::json; int main() { // ["key", 1] const json j1 = {"key", 1}; std::cout << j1 << std::endl; // ["key", 2] const json j2{"key", 2}; std::cout << j2 << std::endl; // {"key":3} const json j3 = {{"key", 3}}; std::cout << j3 << std::endl; // ["key",4] const json j4 = std::pair<const char*, int>{"key", 4}; std::cout << j4 << std::endl; // {"key":5} const json j5{std::pair<const char*, int>{"key", 5}}; std::cout << j5 << std::endl; // {"key1":6,"key2":6} const json j6 = {{"key1", 6}, {"key2", 6}}; std::cout << j6 << std::endl; // [{"key1":7,"key2":7}] const json j7 = {{{"key1", 7}, {"key2", 7}}}; std::cout << j7 << std::endl; return 0; }
#include <nlohmann/json.hpp> #include <iostream> using json = nlohmann::json; void show(const json &x, int i = -1) { std::cout << x.dump(i) << std::endl; } int main() { const json message = R"( { "id": 42, "text": "Hello" } )"_json; // [ // "message", // { // "id": 42, // "text": "Hello" // } // ] const json j1 = {"message", message}; show(j1, 2); // { // "message": { // "id": 42, // "text": "Hello" // } // } const json j2 = {{"message", message}}; show(j2, 2); // { // "messages": [ // { // "id": 42, // "text": "Hello" // }, // { // "id": 42, // "text": "Hello" // } // ] // } const json j3 = {{"messages", {message, message}}}; show(j3, 2); // [ // "messages", // [ // { // "id": 42, // "text": "Hello" // }, // { // "id": 42, // "text": "Hello" // } // ] // ] const json j4 = {"messages", {message, message}}; show(j4, 2); return 0; }
データ取得
#include <iostream> #include <nlohmann/json.hpp> using json = nlohmann::json; int main() { json j = { {"key", "value"}, {"array", {3, 1, 4}}, }; // キー"key"の存在確認をせずに書き換え // {"key" : "value"} => {"key" : 42} j["key"] = 42; // キー"array"の存在確認 // .contains("array") -> bool でもOK if (auto itr = j.find("array"); itr != j.end()) { // 型チェック if (itr->is_array()) { // キー"array"の値を参照 // [3, 1, 4] const json &array = *itr; // (範囲チェックせずに)要素を参照 // [3, @1, 4] const json &v1 = array.at(1); // (型チェックせずに)値を取得 // 1 const int num = v1.get<int>(); // 内部表現を参照 // [3, 1, 4] std::vector<json> &vec = itr->get_ref<json::array_t &>(); // => [3, 1, 4, 1] vec.push_back(num); } } // インデント幅4でシリアライズ const std::string serialized = j.dump(4); std::cout << serialized << std::endl; }
任意の構造体との相互変換
型T
がデフォルト構築可能ならば、表明static_assert(std::default_initializable<T>);
を満足する。
すなわち、T x;
が妥当である。
たとえば、int x;
は妥当であるが、const int x;
は妥当ではない。
デフォルト構築可能な場合
ここにデフォルト構築可能なstruct message
がある。これをjsonと相互変換したい。
struct message { int id; std::string text; }; static_assert(std::default_initializable<message>);
リファレンスによると、デフォルト構築可能な型T
<=>nlohmann::json
の変換に必要なものはこの二つらしい:
void to_json(nlohmann::json &dst, const T &src);
void from_json(const nlohmann::json &src, T &dst);
この二つはclassのinterfaceではないから、Hidden Friendsで書くといいと思う(私の浅知恵)。
このようにする:
struct message { int id; std::string text; private: using json = ::nlohmann::json; friend void to_json(json &dst, const message &src) noexcept { dst["id"] = src.id; dst["text"] = src.text; } // 変換できない場合の例外をcatchできるようにしておく friend void from_json(const json &src, message &dst) { src.at("id").get_to(dst.id); src.at("text").get_to(dst.text); } }; static_assert(std::default_initializable<message>);
このように動く:
{ const message origin = { .id = 42, .text = "Hello" }; const json j = origin; // to_json() const message m = j; // from_json() } { const message origin = { .id = 42, .text = "Hello" }; const json j = static_cast<json> (origin); const message m1 = static_cast<message>(j); const message m2 = j.get <message>(); message m3; j.get_to(m3); }
デフォルト構築不可能な場合
ここにデフォルト構築不可能なstruct message
がある。これをjsonと相互変換したい。
class message { int id_; std::string text_; public: message() = delete; template <typename... Args> requires std::constructible_from<std::string, Args...> message(int id, Args... args) : id_{id} , text_{args...} {} auto &id() const noexcept { return this->id_; } auto &text() const noexcept { return this->text_; } }; static_assert(not std::default_initializable<message>);
リファレンスによると、デフォルト構築不可能な型T
<=>nlohmann::json
の変換に必要なものはこの二つらしい:
namespace ::nlohmann { template <> struct adl_serializer<T> { void to_json(nlohmann::json &dst, const T &src); void from_json(const nlohmann::json &src); }; } // ::nlohmann
もしto_json()
のみが必要であれば、Hidden Friends
での実装でも動く。
しかし、to_json()
をHidden Friends
で実装し、from_json()
を特殊化で実装すると動かない。
このようにする:
template <> struct nlohmann::adl_serializer<message> { static void to_json(json &dst, const message &src) noexcept { dst["id"] = src.id(); dst["text"] = src.text(); } // 変換できない場合の例外をcatchできるようにしておく static message from_json(const json &src) { return { src.at("id").get<int>(), src.at("text").get<std::string>(), }; } };
このように動く:
{ const message origin = {42, "Hello"}; const json j = origin; // to_json() const message m = j; // from_json() } { const message origin = {42, "Hello"}; const json j = static_cast<json> (origin); const message m1 = static_cast<message>(j); const message m2 = j.get <message>(); // デフォルト構築不可能 // message m3; // j.get_to(m3); }
ソート
nlohmann::json
はランダムアクセスイテレーターを満たしていないらしいので、std::ranges::sort()
を利用できない。
しかし、内部表現の配列std::vector<nlohmann::json>
を参照することで、std::ranges::sort()
を利用できる。
ただし、暗黙に型変換を行うので、比較関数の扱いが厄介だ。
#include <iostream> #include <nlohmann/json.hpp> using json = nlohmann::json; int main() { json j = R"( {"array" : [3, 1, 4, 1, 5]} )"_json; { std::sort(j.at("array").begin(), j.at("array").end()); // {"array":[1,1,3,4,5]} std::cout << j.dump() << std::endl; } { // 内部表現を参照 std::vector<json> &array = j.at("array").get_ref<json::array_t &>(); // int型として降順ソート std::ranges::sort(array, std::greater<int>{}); // {"array":[5,4,3,1,1]} std::cout << j.dump() << std::endl; } return 0; }
シリアライゼーション
プログラム中のデータを、ファイルやネットワークへ書き出したり読みだしたりするときにシリアライゼーションをする。
シリアライズせずにメモリに展開された生のbit列をそのまま送出すると、バイトオーダー(エンディアン)が異なる場合に、解釈に齟齬が発生する可能性がある。
バイナリを書き出す場合は、std::cout.write(bin.data(), bin.size()) << std::flush;
のようにすると、ヌル文字を超えられる。
const std::uint8_t *
では書き出せないので、reinterpret_cast<const char *>
する。
文字列
文字列へのシリアライズには.dump(int) -> std::string
を使う。
文字列からのデシリアライズにはstatic json::parse(str/file) -> json
を使う。
よくある.json
ファイルとして扱える。
MessagePack
#include <nlohmann/json.hpp> #include "message.hpp" using json = nlohmann::json; int main() { const message msg1 = {.id = 1, .text = "aaa"}; const message msg2 = {.id = 2, .text = "bb"}; const message msg3 = {.id = 3, .text = "c"}; const json messages = {{"messages", {msg1, msg2, msg3}}}; const std::vector<std::uint8_t> msgpack = json::to_msgpack(messages); const json j = json::from_msgpack(msgpack); const std::vector<message> vec = j.at("messages"); return 0; }
CBOR
#include <nlohmann/json.hpp> #include "message.hpp" using json = nlohmann::json; int main() { const message msg1 = {.id = 1, .text = "aaa"}; const message msg2 = {.id = 2, .text = "bb"}; const message msg3 = {.id = 3, .text = "c"}; const json messages = {{"messages", {msg1, msg2, msg3}}}; const std::vector<std::uint8_t> cbor = json::to_cbor(messages); const json j = json::from_cbor(cbor); const std::vector<message> vec = j.at("messages"); return 0; }
バイナリ型に符号化方式を埋め込む
nlohmann/json
のシリアライズは、std::string
かstd::vector<std::uint8_t>
を返す。
すなわち、良くも悪くも、MessagePack
とCBOR
が同じ型で表現されることを意味する。
ゆえに、以下のコードはコンパイルエラーとならない。
#include <iostream> #include <nlohmann/json.hpp> #include "message.hpp" using json = ::nlohmann::json; int main() { const message msg1 = {.id = 1, .text = "aaa"}; const message msg2 = {.id = 2, .text = "bb"}; const message msg3 = {.id = 3, .text = "c"}; const json messages = {{"messages", {msg1, msg2, msg3}}}; const std::vector<std::uint8_t> msgpack = json::to_msgpack(messages); const json j = json::from_cbor(msgpack); // XXX const std::vector<message> vec = j.at("messages"); return 0; }
ということで、オレオレラップする。
使用例:
const binary_data b = binary_data::from_json(j); const json j = binary_data::to_json(b);
basic_binary_data.hpp
#include <nlohmann/json.hpp> #include <ranges> #include <vector> using json = ::nlohmann::json; /// @tparam Encording msgpack_encording | cbor_encording | json_stringify | etc... in encording.hpp /// @tparam CharT char | unsgined char | std::size_t | std::uint8_t | etc... template <typename Encording, typename CharT = char> requires (sizeof(CharT) == 1) class basic_binary_data { public: using encording = Encording; using value_type = CharT; using pointer = value_type *; using const_pointer = const value_type *; using reference = value_type &; using const_reference = const value_type &; using iterator = value_type *; using const_iterator = const value_type *; using size_type = std::size_t; static json to_json(const basic_binary_data &x) { return encording::to_json(x.begin(), x.end()); } static basic_binary_data from_json(const json &x) { // vecの型はstd::vector<T>のみならず、std::stringでも可 const auto &vec = encording::from_json(x); // vecのvalue_typeがbasic_binary_dataのvalue_typeと一致しない場合への対処 constexpr auto adapter = std::views::transform([](auto a) { return static_cast<value_type>(a); }); return basic_binary_data{vec | adapter}; } basic_binary_data() noexcept : binary_data_{} {} basic_binary_data(std::size_t n) noexcept : binary_data_(n) {} auto begin() noexcept { return this->binary_data_.begin(); } auto begin() const noexcept { return this->binary_data_.begin(); } auto end() noexcept { return this->binary_data_.end(); } auto end() const noexcept { return this->binary_data_.end(); } auto size() const noexcept { return this->binary_data_.size(); } auto data() noexcept { return this->binary_data_.data(); } auto data() const noexcept { return this->binary_data_.data(); } void resize(std::size_t n) noexcept { this->binary_data_.resize(n); } // value_typeにかかわらず、確実にconst char*を返す // ただし、null終端を保証しない // sizeとの併用を推奨する auto c_str() noexcept { return reinterpret_cast<char *>(this->binary_data_.data()); } auto c_str() const noexcept { return reinterpret_cast<const char *>(this->binary_data_.data()); } private: basic_binary_data(const std::ranges::range auto &rng) noexcept : binary_data_{rng.begin(), rng.end()} {} private: std::vector<value_type> binary_data_; };
encording.hpp
#include <nlohmann/json.hpp> using json = ::nlohmann::json; struct msgpack_encording { template <typename Iterator> static json to_json(Iterator first, Iterator last) { return json::from_msgpack(first, last); } static std::vector<std::uint8_t> from_json(const json &x) { return json::to_msgpack(x); } }; struct cbor_encording { template <typename Iterator> static json to_json(Iterator first, Iterator last) { return json::from_cbor(first, last); } static std::vector<std::uint8_t> from_json(const json &x) { return json::to_cbor(x); } }; struct json_stringify { template <typename Iterator> static json to_json(Iterator first, Iterator last) { return json::parse(first, last); } static std::string from_json(const json &x) { return x.dump(); } }; // struct your_encording {};
binary_data.hpp
#include "basic_binary_data.hpp" #include "encording.hpp" // いずれか一つ、お好みの符号化方式を使用する // JSON using binary_data = basic_binary_data<json_stringify>; // MessagePack // using binary_data = basic_binary_data<msgpack_encording>; // CBOR // using binary_data = basic_binary_data<cbor_encording>;
使い方:
main.cpp
#include <filesystem> #include <fstream> #include <iostream> #include <nlohmann/json.hpp> #include "binary_data.hpp" #include "message.hpp" using json = ::nlohmann::json; int main() { const message msg1 = {.id = 1, .text = "aaa"}; const message msg2 = {.id = 2, .text = "bb"}; const message msg3 = {.id = 3, .text = "c"}; const json messages = {{"messages", {msg1, msg2, msg3}}}; const std::filesystem::path path = [](std::string &&fname) { fname += "."; fname += binary_data::encording::extension; return std::move(fname); }("a"); // シリアライズしてファイルに書き出す { const binary_data b = binary_data::from_json(messages); std::ofstream ofs(path); ofs.write(b.c_str(), b.size()); ofs.flush(); } // ファイルから読み込む { const std::uintmax_t size = std::filesystem::file_size(path); binary_data b(size); std::ifstream ifs(path); ifs.read(b.c_str(), b.size()); const json j = binary_data::to_json(b); } return 0; }
パッチ
#include <iostream> #include <nlohmann/json.hpp> using json = nlohmann::json; int main() { const json origin = { {"key", "value"}, }; std::cout << origin.dump() << std::endl; const json patch = { { {"op", "replace"}, {"path", "/key"}, {"value", 42}, }, }; // originを変更する場合は、.patch_inplace(patch)を使う const json patched = origin.patch(patch); std::cout << patched.dump() << std::endl; return 0; }
{"key":"value"} {"key":42}
まとめ
ここが一番よくまとまっています。