2026-07-05 17:18:26
諏訪子
c++
json

【C++】JSONライブラリをゼロから作成

JSONはよく知られているので、説明は不要でしょう。
JSONは元々JavaScriptの為だけに作られましたが、其の後全てのプログラミング言語に広がり、XML、YAML、INIと共に使われています。
殆どの高級言語は標準ライブラリに採用しています。
PHPではjson_encode()json_decode、Goではencoding/json、Zigではstd.json、C#ではSystem.Text.Jsonです。
Rustだけはいつもの様に第三者ライブラリが必要です。

然し、C、C++、Assembly等の低級言語では、自分で作る必要があります。
殆どのC++開発者はNlohmann JSONを使います。
此れは軽量でポータブル、インクルードも簡単だからです。
でも、自分で作ってみたらどうでしょう?

公式仕様は此方にあります。
又、あたしが完成させたバージョンは此方です。

注意点として、此れはJSON標準の第2版ですが、第1版と技術的な違いはありません。
第1版と第2版の違いは、仕様の表現方法、「JSON」の発音方法、導入部と著作権セクションの細かい修正だけです。
あたし達にとって重要な部分に違いはありません。

此のチュートリアルでは、自分だけのJSONライブラリをどれだけ簡単に作れるかを紹介します。
但し、あたしおのJSONライブラリと異なる点が1つあります:ファイルからのパース機能は含めません。
1.0.0のリリース後に、ファイル処理を含めるのは悪いアイデアだと気づきました。
理由は以下の通りです:

  1. ポータブルではない。パソコン用のOSでは動作しますが、スマホやゲーム機では動作しません。
  2. 自分でファイルを開閉するのはとても簡単なので、ライブラリの行数を無駄に増やすだけです。

あたしのライブラリがNintendo SwitchとSwitch 2でコンパイル出来なかったのを見て気づきました。
其処でfile.hhfile.cc全体を任天堂のファイルAPIに合わせて修正する必要がありました。
iOS、Android、PS5、Xbox用にも別バージョンを作る必要が出てくると思います。
なので、1.0.1のリリースでは削除しました。

依存関係

ライブラリは一切使いません。
完全にゼロから作るからです。
但し、C++20以降をサポートするC++コンパイラが必要です。GCC、Clang、MSVCの最新版なら問題ありません。
又、CMakeと、お気に入りのテキストエディタまたはIDEもインストールして下さい。

Neovim、Emacs、又は其の他clangdを使えるエディタの場合:

CompileFlags:
  Add:
    - -I./include
    - -std=c++20
    - -Wno-pragma-once-outside-header

ディレクトリ構造も設定しましょう。

$ mkdir include src tests include/suwa
$ touch CMakeLists.txt tests/main.cc include/suwa/json.hh src/json.cc

CMakeLists.txtの内容:

cmake_minimum_required(VERSION 3.14...3.28)
project(json LANGUAGES CXX VERSION 1.0.0)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

set(BUILD_SHARED_LIBS OFF CACHE BOOL "静的リンク" FORCE)

option(ENABLE_TEST "テストプロジェクト" ON)

if(MSVC) 
  set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
  add_compile_options(/utf-8)
endif()

# 本ライブラリ
add_library(json STATIC)
file(GLOB_RECURSE LIBJSON_SRC CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cc")
file(GLOB_RECURSE LIBJSON_INC CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/include/*.hh")

target_sources(json
  PRIVATE "${LIBJSON_SRC}"
  PUBLIC "${LIBJSON_INC}"
)

target_include_directories(json PUBLIC
  "${CMAKE_CURRENT_SOURCE_DIR}/include"
)

# テスト用
if(ENABLE_TEST)
  configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/tests/webfinger.json
    ${CMAKE_CURRENT_BINARY_DIR}/webfinger.json
    COPYONLY
  )
  
  configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/tests/activitypub.json
    ${CMAKE_CURRENT_BINARY_DIR}/activitypub.json
    COPYONLY
  )
  
  configure_file(
    ${CMAKE_CURRENT_SOURCE_DIR}/tests/gltf.json
    ${CMAKE_CURRENT_BINARY_DIR}/gltf.json
    COPYONLY
  )

  add_executable(json_test)
  file(GLOB_RECURSE TEST_SRC CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/*.cc")
  file(GLOB_RECURSE TEST_INC CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/tests/*.hh")
  target_sources(json_test PRIVATE ${TEST_SRC})
  target_link_libraries(json_test PRIVATE json)

  if(MSVC)
    set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT json_test)
  endif()
endif()

# OS
if(UNIX)
  include(GNUInstallDirs)
  set(INSTALL_BINDIR ${CMAKE_INSTALL_LIBDIR})
endif()

if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
  set(PRODUCTION_BUILD OFF CACHE BOOL "リリース版?" FORCE)
else()
  set(PRODUCTION_BUILD ON CACHE BOOL "リリース版?" FORCE)
endif()

install(TARGETS json
  RUNTIME DESTINATION ${INSTALL_BINDIR}
  PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE
  COMPONENT Runtime
)

install(DIRECTORY DESTINATION ${INSTALL_DATADIR}
  COMPONENT Runtime
)

if(MSVC)
  target_compile_definitions(json PUBLIC _CRT_SECURE_NO_WARNINGS)
elseif(MINGW)
  if(PRODUCTION_BUILD)
    target_link_options(json PRIVATE -mwindows)
  endif()
elseif(APPLE OR UNIX)
  if(PRODUCTION_BUILD)
    target_link_options(json PRIVATE -s)
  endif()
endif()

テスト用のJSONファイルは此方からダウンロードしてtestsディレクトリに入れて下さい:

JSON標準

始める前に、非常に短いJSON仕様書を簡単に確認しましょう。

JSONテキスト

JSONには6つの構造トークンがあります:[, ], {, }, :, ,
又、3つのリテラル名トークンがあります:true, false, null
意味のない空白はどのトークンの前後にも許されますが、重要な部分はエスケープする必要があります。
文字列の中ではスペースが許されますが、トークンの中では許されません。
従ってtr ueの様な物は無効です。

JSON値

値には7種類あります:オブジェクト、配列、数値、文字列、truefalsenull

JSONオブジェクト

オブジェクトは波括弧トークンで囲まれた0個以上のキー/値ペアです。
キーの後にコロン(:)、値、最後に(最後でなければ)カンマが続きます。
キーとして使う文字列に制限はなく、一意である必要も、順序がある必要もありません。
各JSONプロセッサは自由に定義出来ます。

JSON配列

配列は角括弧トークンで囲まれた0個以上の値です。
値はカンマで区切られますが、最後の値にはカンマを付けません。
順序に制限はありません。

JSON数値

数値は0から9の10進数字で、マイナス(-)符号を前に付けてもよく、小数点、指数(eまたはE)、および+または-が使えます。
但し、InfinityNaNは許されません。

JSON文字列

文字列は二重引用符(")で囲みます。
エスケープ文字が使えます。
エスケープ文字は以下の通りです:

  • 二重引用符(\")
  • エスケープ文字(\\)
  • スラッシュ(\/)
  • バックスペース(\b)
  • フォームフィード(\f)
  • 改行(\n)
  • 復帰(\r)
  • 水平タブ(\t)
  • 16進数プレフィックス(\u)

16進数については、次の4つは全て同じ結果に成ります:

  • "\u002F"
  • "\u002f"
  • "\/"
  • "/"

ルールとして、16進数は必ず4バイトで表現する必要があります。
詰り\u002Fは有効ですが\u2Fが無効です。

JSONライブラリ

JSONライブラリを作り始めましょう。
先ずinclude/suwa/json.hhを開きます。
多くの人は#pragma onceから始めます。
然し、複数のプロジェクトで使うライブラリでは#ifndef (ライブラリ名)_HH#define (ライブラリ名)_HH#endifを好みます。
単独プロジェクトでは#pragma onceだけを使います。
実用的には何方でも構いません。
#pragma onceは標準ではありませんが、ほぼ全てのC/C++コンパイラでサポートされています。
何方を使うかは自由に決めて下さい。

#ifndef JSON_HH
#define JSON_HH

#include <util/types.hh>
#include <unordered_map>
#include <string_view>
#include <variant>
#include <optional>
#include <memory>
#include <string>
#include <vector>

namespace json {
  enum class Error {
    None,
    SyntaxError,
    UnexpectedEnd,
    InvalidValue,
    InvalidNumber,
    UnterminatedString,
  }; // enum class Error

  struct SerializeOptions {
    bool pretty = false;
    size_t indent = 2;
  };
} // namespace json

#endif // JSON_HH

C++で作業する時は、ライブラリを名前空間で囲む事が重要です。
此れにより構文が読み易く成り、特に他のライブラリとの名前の衝突を避けられます。
C言語には名前空間がないので、関数名にライブラリ名をプレフィックスするのが一般的です。
C++ではgame::Engine e; e.Init()の様にgameが名前空間、Engineがクラスまたは構造体、Init()が公開メンバメソッドに成ります。
C言語ではgame_engine_init(struct Engine *e)という関数に成り、エンジンのプロパティを持つ構造体を使います。
C++ではconst Engine &eをパラメータとして渡す必要はありません。
コンストラクタで既に済んでおり、プロパティはメンバ変数でアクセスします。

次にenum classstructを追加しました。
enum classを使う事が非常に重要です。
複数のenumがあり、何方にもNoneがあると名前の衝突が起き、コンパイルエラーに成ります。
C++ではenumの代わりにenum classを使う事で其れを避けられます。
C言語ではERROR_NONEの様にプレフィックスします。

次に説明する部分で多くの人が躓くでしょう。
ObjectValueクラスはお互いに依存しているので、前方宣言が必要です。
C++コンパイラは上から下に読む為、定義する順番通りに前方宣言する必要があります。
そうしないとコンパイルエラーに成ります。

  struct ParseResult;
  struct StringHash;
  class Object;
  class Value;

  struct ParseResult {
    std::unique_ptr<Value> value;
    Error error = Error::None;
    size_t errPos = 0;
    bool ok() const { return error == Error::None; }
  }; // struct ParseResult

  struct StringHash {
    using is_transparent = void;
    size_t operator()(std::string_view sv) const {
      return std::hash<std::string_view>{}(sv);
    }
  }; // struct StringHash

  class Object {
    public:
      Object() = default;

      Object(const Object &) = delete;
      Object &operator=(const Object &) = delete;

      Object(Object &&) noexcept = default;
      Object &operator=(Object &&) noexcept = default;

      Value &operator[](std::string_view key);
      const Value &operator[](std::string_view key) const;

      void insert(std::string key, Value value);
      Value *get(std::string_view key);
      const Value *get(std::string_view key) const;
      bool contains(std::string_view key) const;

    public:
      bool empty() const { return m_Data.empty(); }
      size_t size() const { return m_Data.size(); }
      auto begin() { return m_Data.begin(); }
      auto end() { return m_Data.end(); }
      auto begin() const { return m_Data.begin(); }
      auto end() const { return m_Data.end(); }

    private:
      using Pair = std::pair<std::string, std::unique_ptr<Value>>;
      std::vector<Pair> m_Data;
      std::unordered_map<std::string, size_t, StringHash, std::equal_to<>> m_Index;
  }; // struct Object

  class Value {
    public:
      using Array = std::vector<Value>;
      using Object = json::Object;

      using Variant = std::variant<
        std::monostate, // null
        bool,
        double,
        std::string,
        Array,
        Object
      >;

      // 配列
      Value &operator[](size_t index) {
        if (!is_array()) as_array() = Array{};

        auto &arr = as_array();
        if (index >= arr.size()) arr.resize(index + 1);
        return arr[index];
      }

      // オブジェクト
      Value &operator[](std::string_view key) {
        if (!is_object()) as_object() = Object{};

        auto *val = as_object().get(key);
        if (!val) {
          as_object().insert(std::string(key), Value{});
          val = as_object().get(key);
        }
        return *val;
      }

    public:
      Value() = default;
      Value(std::nullptr_t) : m_Value(std::monostate{}) {}
      Value(bool b) : m_Value(b) {}
      Value(double n) : m_Value(n) {}
      Value(float n) : m_Value(n) {}
      Value(int64_t n) : m_Value(static_cast<double>(n)) {}
      Value(int32_t n) : m_Value(static_cast<double>(n)) {}
      Value(uint64_t n) : m_Value(static_cast<double>(n)) {}
      Value(uint32_t n) : m_Value(static_cast<double>(n)) {}
      Value(std::string s) : m_Value(std::move(s)) {}
      Value(const char *s) : m_Value(std::string(s)) {}
      Value(Array &&arr) : m_Value(std::move(arr)) {}
      Value(Object &&obj) : m_Value(std::move(obj)) {}

    public:
      bool is_null() const { return std::holds_alternative<std::monostate>(m_Value); }
      bool is_bool() const { return std::holds_alternative<bool>(m_Value); }
      bool is_number() const { return std::holds_alternative<double>(m_Value); }
      bool is_string() const { return std::holds_alternative<std::string>(m_Value); }
      bool is_array() const { return std::holds_alternative<Array>(m_Value); }
      bool is_object() const { return std::holds_alternative<Object>(m_Value); }

    public:
      bool as_bool() const { return std::get<bool>(m_Value); }
      double as_number() const { return std::get<double>(m_Value); }
      std::string &as_string() { return std::get<std::string>(m_Value); }
      const std::string &as_string() const { return std::get<std::string>(m_Value); }
      Array &as_array() { return std::get<Array>(m_Value); }
      const Array &as_array() const { return std::get<Array>(m_Value); }
      Object &as_object() { return std::get<Object>(m_Value); }
      const Object &as_object() const { return std::get<Object>(m_Value); }

    public:
      std::optional<bool> get_bool() const;
      std::optional<double> get_number() const;
      std::optional<std::string> get_string() const;

    public:
      static ParseResult parse(std::string_view jsonText);

    public:
      std::string serialize(const SerializeOptions &opts = {}) const;

    private:
      static std::string make_indent(size_t depth, size_t width);

    private:
      std::string serialize_impl(const SerializeOptions &opts, size_t depth) const;
      static std::string serialize_string(const std::string &s);

    private:
      Variant m_Value;
  }; // class Value

最初の2つの構造体はか成単純です。
値とメンバ関数を全て宣言・定義しています。
std::unique_ptr<Value>(Value *)のより安全な物です。
通常、ゲームエンジンではパフォーマンスの為にC言語スタイルの生ポインタが推奨されます。
然し、此のライブラリはシーン開始時の数フレームでglTFモデルをロードしたり、JSONデータを保存したりするだけなので、スマートポインタを使っても全く問題ありません。
JSONオブジェクトに常時アクセスするなら、生ポインタの方が良いでしょう。
使用するには#include <memory>が必要です。

using is_transparent = void;typedef void is_transparent;のC++版です。
詰りエイリアスを作成しています。
operator()は演算子オーバーロードです。
此れはC++独自の機能で、+*<<等の演算子の動作を本来とは違う物に変更出来ます。
とても便利です。
最も一般的な使い方はClass &operator=(const Class &) = delete;Class &operator=(Class &&) noexcept = default;です。
此れによりクラスのコピーを不可能にし、バグのクラス全体を防ぎます。
但し、此れはクラスと構造体の中でのみ機能するので、他のコードやライブラリ、実行プログラムで奇妙な動作を起こしません。
最後に、関数名の後とパラメータの後、スコープの前にconstを付ける事で定数にしています。

未だ理解出来ていない場合、C++ではクラスと構造体は本質的に同じ物です。
唯一の違いは、クラスのメンバはデフォルトでprivate、構造体のメンバはデフォルトでpublicである点です。

次にObjectクラスに移ります。
此処で演算子オーバーロードの良い例が見られます:Value &operator[](std::string_view key);const Value &operator[](std::string_view key) const;
此れにより後で定義する時にobj["key"]の様に書けます。

public:private:を複数回使っている事に気づいたでしょう。
此れは完全に許されており、変数とメソッド、宣言だけとクラス内で定義するメソッド等、メンバの種類を視覚的に分ける為に使っています。

又、std::pair<>std::unordered_map<>std::equal_to<>等、見た事のないC++コンテナも出てきます。
unordered_mapは高級言語のオブジェクトの様な物で、指数はありません。
指数が必要ならstd::map<>を使います。
但し、unordered_mapの方がパフォーマンスが高く、指数を順序付けする必要もありません。

pairは2つの要素(此処ではstd::stringValueへのunique pointer)を保持します。
equal_toは比較を行います。

次にValueクラスで面白い部分が出てきます。
先ずArraystd::vector<Value>に、Objectjson::Objectにエイリアスしています。
特に変わった事はありません。
次にVariantstd::variant<>にエイリアスします。
std::variant<>は型安全なunionです。
参照、配列、voidは保持出来ません。
std::monostateは、デフォルト構築不可能な型のvariantの最初の代替として使うプレースホルダ型です。
此処ではnullptr用です。

次に多くの型のコンストラクタをオーバーロードしています。
整数を浮動小数点数として認識したり、const char *std::stringとして扱うなど、複数の型をカバーする為です。
std::move<>は名前の通り変数を「移動」するわけではありません。
変数が「移動元」に成る事を示し、リソースの効率的な転送を可能にします。
要するに、コンパイラに「此れが終わったら他のオブジェクトが盗んで良いよ」と伝えます。

std::holds_alternative<>はvariant(値)が指定した代替型(類)を持っているかを確認します(variantの型リストに正確に1つだけある場合)。
std::get<>はタプルの要素を取り出します。
std::optional<>は値が存在するかもしん管理します。

又、static ParseResult parse()std::string serialize()に注目して下さい。
此れらはJSONオブジェクトを文字列に変換して出力する為の最も重要なメソッドです。

  class Parser {
    public:
      Parser(std::string_view text) : m_Text(text), m_Pos(0) {}

      ParseResult parse();

    private:
      std::string_view m_Text;
      size_t m_Pos;

      ParseResult make_error(Error err) const;
      void skip_whitespace();
      char peek() const;
      char consume();
      bool expect(char c);

      ParseResult parse_value();
      ParseResult parse_object();
      ParseResult parse_array();
      ParseResult parse_string();
      std::optional<double> parse_number();
  }; // class Parser
...
inline std::ostream &operator<<(std::ostream &os, const json::Value &v) {
  os << v.serialize();
  return os;
}

inline json::Value &json::Object::operator[](std::string_view key) {
  return *get(key);
}

inline const json::Value &json::Object::operator[](std::string_view key) const {
  return *get(key);
}

最後にParserクラスと、先ほど宣言した演算子の定義があります。

parse()は此のクラスで最も重要なメソッドで、JSONオブジェクトを受け取り、C++内で使える形に変換します。
通常、公開メソッドは大文字、あたし用メソッドは小文字で定義しますが、此のライブラリを作っている時にC++のvectorメソッドを思い出したので、vectorに合わせる為に全て小文字にしました。

此れでヘッダは完成です。
次にソースファイルに移ります。

#include <suwa/json.hh>
#include <charconv>
#include <utility>
#include <cassert>
#include <sstream>

namespace json {
  ParseResult Parser::make_error(Error err) const {
    return { std::make_unique<Value>(), err, m_Pos };
  }

  void Parser::skip_whitespace() {
    while (m_Pos < m_Text.size()) {
      char c = m_Text[m_Pos];
      if (c != ' ' && c != '\t' && c != '\n' && c != '\r') break;
      ++m_Pos;
    }
  }

  char Parser::peek() const {
    return m_Pos < m_Text.size() ? m_Text[m_Pos] : '\0';
  }

  char Parser::consume() {
    return m_Pos < m_Text.size() ? m_Text[m_Pos++] : '\0';
  }

  bool Parser::expect(char c) {
    if (peek() == c) {
      consume();
      return true;
    }

    return false;
  }

  ParseResult Parser::parse_value() {
    skip_whitespace();
    if (m_Pos >= m_Text.size()) return make_error(Error::UnexpectedEnd);

    char c = peek();
    if (c == '{') return parse_object();
    if (c == '[') return parse_array();
    if (c == '"') {
      auto str = parse_string();
      if (!str.ok()) return str;
      return str;
    }

    if (m_Text.substr(m_Pos, 4) == "true") {
      m_Pos += 4;
      return { std::make_unique<Value>(true) };
    }

    if (m_Text.substr(m_Pos, 5) == "false") {
      m_Pos += 5;
      return { std::make_unique<Value>(false) };
    }

    if (m_Text.substr(m_Pos, 4) == "null") {
      m_Pos += 4;
      return { std::make_unique<Value>(nullptr) };
    }

    if ((c >= '0' && c <= '9') || c == '-' || c == '+') {
      auto num = parse_number();
      if (num) return { std::make_unique<Value>(*num) };
      return make_error(Error::InvalidNumber);
    }

    return make_error(Error::InvalidValue);
  }

  ParseResult Parser::parse_object() {
    if (!expect('{')) return make_error(Error::SyntaxError);

    Object obj;
    skip_whitespace();

    if (peek() == '}') {
      consume();
      return { std::make_unique<Value>(std::move(obj)) };
    }

    while (true) {
      skip_whitespace();
      auto keyres = parse_string();
      if (!keyres.ok()) return keyres;

      skip_whitespace();
      if (!expect(':')) return make_error(Error::SyntaxError);

      auto valres = parse_value();
      if (!valres.ok()) return valres;

      obj.insert(
        std::move(keyres.value->as_string()),
        std::move(*valres.value)
      );

      skip_whitespace();
      if (peek() == '}') {
        consume();
        break;
      }
      if (!expect(',')) return make_error(Error::SyntaxError);
    }

    return { std::make_unique<Value>(std::move(obj)) };
  }

  ParseResult Parser::parse_array() {
    if (!expect('[')) return make_error(Error::SyntaxError);

    Value::Array arr;
    skip_whitespace();

    if (peek() == ']') {
      consume();
      return { std::make_unique<Value>(std::move(arr)) };
    }

    while (true) {
      auto valres = parse_value();
      if (!valres.ok()) return valres;
      arr.push_back(std::move(*valres.value));

      skip_whitespace();
      if (peek() == ']') {
        consume();
        break;
      }
      if (!expect(',')) return make_error(Error::SyntaxError);
    }

    return { std::make_unique<Value>(std::move(arr)) };
  }

  ParseResult Parser::parse_string() {
    if (!expect('"')) return make_error(Error::SyntaxError);

    std::string res;
    res.reserve(32);

    while (m_Pos < m_Text.size()) {
      char c = consume();
      if (c == '"') return { std::make_unique<Value>(std::move(res)) };
      if (c == '\\') {
        if (m_Pos >= m_Text.size()) make_error(Error::UnterminatedString);
        char esc = consume();
        switch (esc) {
          case '"': res += '"'; break;
          case '\\': res += '\\'; break;
          case '/': res += '/'; break;
          case 'b': res += '\b'; break;
          case 'f': res += '\f'; break;
          case 'n': res += '\n'; break;
          case 'r': res += '\r'; break;
          case 't': res += '\t'; break;
          case 'u': {
            for (int i = 0; i < 4; ++i) {
              if (m_Pos >= m_Text.size())
                return make_error(Error::UnterminatedString);

              char h = consume();

              if (!std::isxdigit((unsigned char)h))
                return make_error(Error::SyntaxError);
            }

            break;
          }
          default: return make_error(Error::SyntaxError);
        }
      } else {
        res += c;
      }
    }

    return make_error(Error::UnterminatedString);
  }

  std::optional<double> Parser::parse_number() {
    const char*start = m_Text.data() + m_Pos;
    double num = 0.0;
    char *end = nullptr;
    num = std::strtod(start, &end);

    if (end == start) return std::nullopt;

    m_Pos += (end - start);
    return num;
  }
  
  // 公開API
  ParseResult Value::parse(std::string_view jsonText) {
    Parser p(jsonText);
    return p.parse();
  }

  ParseResult Parser::parse() {
    skip_whitespace();
    ParseResult res = parse_value();
    if (!res.ok()) return res;

    skip_whitespace();
    if (m_Pos < m_Text.size()) {
      assert(false && "JSON値の後でデータがある");
    }

    return res;
  }
} // namespace json

Parserクラスから始めます。
大部分は単純ですが、全てのprivateメソッドはJSON標準に完全に準拠しています。
ゲーム機との互換性を保つ為に、例外を意図的に投げない様にしています。

  std::string Value::serialize(const SerializeOptions &opts) const {
    return serialize_impl(opts, 0);
  }
  
  std::string Value::serialize_impl(const SerializeOptions &opts, size_t depth) const {
    if (is_null())   return "null";
    if (is_bool())   return as_bool() ? "true" : "false";
    if (is_number()) {
      std::ostringstream oss;
      oss << as_number();
      return oss.str();
    }
    if (is_string())
      return serialize_string(as_string());

    if (is_array()) {
      const auto &arr = as_array();

      std::string out = "[";
      if (opts.pretty && !arr.empty())
        out += "\n";

      for (size_t i = 0; i < arr.size(); ++i) {
        if (opts.pretty)
          out += make_indent(depth + 1, opts.indent);

        out += arr[i].serialize_impl(opts, depth + 1);

        if (i + 1 < arr.size())
          out += ",";

        if (opts.pretty)
          out += "\n";
      }

      if (opts.pretty && !arr.empty())
        out += make_indent(depth, opts.indent);

      out += "]";
      return out;
    }

    if (is_object()) {
      const auto &obj = as_object();

      std::string out = "{";

      if (opts.pretty && !obj.empty())
        out += "\n";

      size_t i = 0;
      for (const auto &[key, val] : obj) {
        if (opts.pretty)
          out += make_indent(depth + 1, opts.indent);

        out += serialize_string(key);
        out += opts.pretty ? ": " : ":";
        out += val->serialize_impl(opts, depth + 1);

        if (++i < obj.size())
          out += ",";

        if (opts.pretty)
          out += "\n";
      }

      if (opts.pretty && !obj.empty())
        out += make_indent(depth, opts.indent);

      out += "}";
      return out;
    }

    return "null";
  }

  std::string Value::make_indent(size_t depth, size_t width) {
    return std::string(depth * width, ' ');
  }
  
  std::string Value::serialize_string(const std::string &s) {
    std::string out = "\"";

    for (char c : s) {
      switch (c) {
        case '"': out += "\\\""; break;
        case '\\': out += "\\\\"; break;
        case '\b': out += "\\b"; break;
        case '\f': out += "\\f"; break;
        case '\n': out += "\\n"; break;
        case '\r': out += "\\r"; break;
        case '\t': out += "\\t"; break;
        default: out += c; break;
      }
    }

    out += "\"";
    return out;
  }

次にValueクラスです。
此処も特に難しくはありません。
注目すべき点は、serializeserialize_implから分離した事です。
此れにより、シリアライズ機能が自分自身を再帰的に呼び出せる様に成、公開APIを複雑にせずに済みます。

  void Object::insert(std::string key, Value value) {
    auto ptr = std::make_unique<Value>(std::move(value));

    auto it = m_Index.find(key);
    if (it != m_Index.end()) {
      m_Data[it->second].second = std::move(ptr);
    } else {
      m_Index[key] = m_Data.size();
      m_Data.emplace_back(std::move(key), std::move(ptr));
    }
  }

  Value *Object::get(std::string_view key) {
    auto it = m_Index.find(key);
    return it != m_Index.end() ? m_Data[it->second].second.get() : nullptr;
  }

  const Value *Object::get(std::string_view key) const {
    auto it = m_Index.find(key);
    return it != m_Index.end() ? m_Data[it->second].second.get() : nullptr;
  }

  bool Object::contains(std::string_view key) const {
    return m_Index.contains(key);
  }

  //////////////

  std::optional<bool> Value::get_bool() const {
    if (auto *b = std::get_if<bool>(&m_Value)) return *b;
    return std::nullopt;
  }

  std::optional<double> Value::get_number() const {
    if (auto *b = std::get_if<double>(&m_Value)) return *b;
    return std::nullopt;
  }

  std::optional<std::string> Value::get_string() const {
    if (auto *b = std::get_if<std::string>(&m_Value)) return *b;
    return std::nullopt;
  }

最後にObjectクラスと、1行に収まらなかったstd::optional<>メソッドです。

此れでライブラリは完成です。
残るはテストの処理だけです。
記事が長く成り過ぎるので、此処ではコードを示しません。
此方で確認出来ます。
要約すると、メモリ上のJSONと3つのJSONファイルから読み込み、parse → serialize → parseの往復テストをしています。
最後に意図的に無効なJSONオブジェクトを設定してエラー処理をテストします。

此のテストファイルはAPIの使い方も示しています。

以上