C++ JSONライブラリ「nlohmann/json」の使い方

本稿のコード例はC++20で示しています。

概要

nlohmann/json

N・ローマンさんのJSONライブラリ(C++11)

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::stringstd::vector<std::uint8_t>を返す。 すなわち、良くも悪くも、MessagePackCBORが同じ型で表現されることを意味する。 ゆえに、以下のコードはコンパイルエラーとならない。

#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}

まとめ

ここが一番よくまとまっています。