現在のブログ
ゲーム開発ブログ (2025年~) Gamedev Blog (2025~)
レガシーブログ
テクノロジーブログ (2018~2024年) リリースノート (2023~2025年) MeatBSD (2024年)
【C++】Creating a JSON Library from Scratch
JSON is well-known, so no explanation is needed.
JSON was originally created only for JavaScript, but later spread to all programming languages and is used alongside XML, YAML, and INI.
Most high-level languages have adopted it in their standard libraries.
In PHP it's json_encode() and json_decode, in Go it's encoding/json, in Zig it's std.json, and in C# it's System.Text.Json.
Only Rust, as usual, requires a third-party library.
However, in low-level languages like C, C++, and Assembly, you need to create it yourself.
Most C++ developers use Nlohmann JSON.
This is because it is lightweight, portable, and easy to include.
But what if you try making it yourself?
The official specification is available here.
Also, the version I completed is here.
Note that this is the 2nd edition of the JSON standard, but there are no technical differences from the 1st edition.
The differences between the 1st and 2nd editions are only in the way the specification is expressed, the pronunciation of "JSON", and minor corrections in the introduction and copyright sections.
There are no differences in the parts important to us.
This tutorial introduces how easily you can create your own JSON library.
However, there is one difference from my JSON library: it does not include parsing from files.
After the release of 1.0.0, I realized that including file processing was a bad idea.
The reasons are as follows:
- It is not portable. It works on PC OSes, but does not work on smartphones or game consoles.
- Opening and closing files yourself is very easy, so it only unnecessarily increases the number of lines in the library.
I realized this after seeing that my library could not be compiled on Nintendo Switch and Switch 2.
There I had to modify the entire file.hh and file.cc to match Nintendo's file API.
I think I'll also need to create separate versions for iOS, Android, PS5, and Xbox.
So I have removed it in 1.0.1.
Dependencies
The library does not use anything.
This is because it is built completely from scratch.
However, you need a C++ compiler that supports C++20 or later.
The latest versions of GCC, Clang, and MSVC are fine.
Also, please install CMake and your favorite text editor or IDE.
For Neovim, Emacs, or other editors that can use clangd:
CompileFlags:
Add:
- -I./include
- -std=c++20
- -Wno-pragma-once-outside-header
Let's also set up the directory structure.
$ mkdir include src tests include/suwa
$ touch CMakeLists.txt tests/main.cc include/suwa/json.hh src/json.cc
Contents of 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()
Please download the test JSON files from here and place them in the tests directory:
JSON Standard
Before starting, let's briefly review a very short JSON specification.
JSON Text
JSON has six structural tokens: [, ], {, }, :, ,.
It also has three literal name tokens: true, false, null.
Meaningless whitespace is allowed before and after any token, but important parts must be escaped.
Spaces are allowed inside strings, but not inside tokens.
Therefore, something like tr ue is invalid.
JSON Values
There are seven types of values: object, array, number, string, true, false, null.
JSON Object
An object is zero or more key/value pairs enclosed in curly brace tokens.
After the key comes a colon (:), the value, and finally a comma (if not the last).
There are no restrictions on the strings used as keys, and they do not need to be unique or ordered.
Each JSON processor can define this freely.
JSON Array
An array is zero or more values enclosed in square bracket tokens.
Values are separated by commas, but the last value does not have a comma.
There are no restrictions on order.
JSON Number
Numbers consist of decimal digits 0-9, may have a minus sign (-) prefix, and can use decimal points, exponents (e or E), and + or -.
However, Infinity and NaN are not allowed.
JSON String
Strings are enclosed in double quotes (").
Escape characters can be used.
The escape characters are as follows:
- Double quote (
") - Escape character (
\) - Slash (
/) - Backspace (
b) - Form feed (
f) - Newline (
n) - Carriage return (
r) - Horizontal tab (
t) - Hexadecimal prefix (
u)
For hexadecimal, the following four all produce the same result:
"u002F""u002f""/""/"
As a rule, hexadecimal must always be represented with exactly 4 bytes.
That is, u002F is valid, but u2F is invalid.
JSON Library
Let's start creating the JSON library.
First, open include/suwa/json.hh.
Many people start with #pragma once.
However, for libraries used in multiple projects, #ifndef (LIBRARY_NAME)_HH, #define (LIBRARY_NAME)_HH, and #endif are preferred.
For single projects, only #pragma once is used.
Practically, either is fine.
#pragma once is not standard, but it is supported by almost all C/C++ compilers.
You are free to choose which one to use.
#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
When working in C++, it is important to wrap the library in a namespace.
This makes the syntax more readable and especially avoids name collisions with other libraries.
C language has no namespaces, so it is common to prefix function names with the library name.
In C++ it becomes something like game::Engine e; e.Init(), where game is the namespace, Engine is a class or struct, and Init() is a public member method.
In C it becomes a function like game_engine_init(struct Engine *e), using a struct that holds the engine's properties.
In C++ there is no need to pass const Engine &e as a parameter.
It is already done in the constructor, and properties are accessed via member variables.
Next I added enum class and struct.
Using enum class is very important.
If there are multiple enums and each has None, name collisions occur and cause compilation errors.
In C++ you can avoid this by using enum class instead of enum.
In C you would prefix like ERROR_NONE.
Many people will stumble on the part explained next.
The Object and Value classes depend on each other, so forward declarations are necessary.
Since the C++ compiler reads from top to bottom, forward declarations must be made in the order of definition.
Otherwise, you will get compilation errors.
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
The first two structs are quite simple.
They declare and define all values and member functions.
std::unique_ptr<Value> is a safer version of (Value *).
Normally, in game engines, raw C-style pointers are recommended for performance.
However, since this library only loads glTF models in a few frames at scene start or saves JSON data, using smart pointers is completely fine.
If you constantly access JSON objects, raw pointers would be better.
You need #include <memory> to use it.
using is_transparent = void; is the C++ version of typedef void is_transparent;.
In other words, it creates an alias.
operator() is operator overloading.
This is a C++-specific feature that allows you to change the behavior of operators like +, *, <<, etc., to something different from the original.
It is very convenient.
The most common uses are Class &operator=(const Class &) = delete; and Class &operator=(Class &&) noexcept = default;.
This makes copying of the class impossible and prevents bugs throughout the class.
However, this only works inside classes and structs, so it does not cause strange behavior in other code, libraries, or executables.
Finally, adding const after the function name, after parameters, and before the scope makes it constant.
If you still don't understand, in C++ classes and structs are essentially the same thing.
The only difference is that class members are private by default, while struct members are public by default.
Next, we move to the Object class.
Here is a good example of operator overloading: Value &operator[](std::string_view key); and const Value &operator[](std::string_view key) const;.
This allows writing obj["key"] later when defining it.
You may have noticed that public: and private: are used multiple times.
This is completely allowed and is used to visually separate different kinds of members such as variables and methods, declarations only, and methods defined inside the class.
Also, unfamiliar C++ containers such as std::pair<>, std::unordered_map<>, std::equal_to<>, etc. appear.
unordered_map is like an object in high-level languages and has no indices.
If you need indices, use std::map<>.
However, unordered_map has better performance and there is no need to order the indices.
pair holds two elements (here a std::string and a unique pointer to Value).
equal_to performs comparisons.
Next comes the interesting part in the Value class.
First, Array is aliased to std::vector<Value>, and Object to json::Object.
Nothing particularly unusual.
Next, Variant is aliased to std::variant<>.
std::variant<> is a type-safe union.
It cannot hold references, arrays, or void.
std::monostate is a placeholder type used as the first alternative for variants that cannot be default-constructed.
Here it is for nullptr.
Next, many constructors are overloaded for different types.
This is to cover multiple types, such as recognizing integers as floating-point numbers or treating const char * as std::string.
std::move<> does not literally "move" a variable as the name suggests.
It indicates that the variable is the "move source" and enables efficient transfer of resources.
In short, it tells the compiler "after this, other objects can steal it".
std::holds_alternative<> checks whether the variant (value) holds the specified alternative type (exactly one in the variant's type list).
std::get<> extracts an element from the tuple.
std::optional<> manages whether a value exists or not.
Also, pay attention to static ParseResult parse() and std::string serialize().
These are the most important methods for converting JSON objects to strings and outputting them.
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);
}
Finally, there is the Parser class and the definitions of the operators declared earlier.
parse() is the most important method in this class.
It takes a JSON object and converts it into a form usable inside C++.
Normally, public methods are defined with capital letters and private methods with lowercase, but while making this library I remembered C++ vector methods, so I made everything lowercase to match vector.
This completes the header.
Next, we move to the source file.
#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
We start with the Parser class.
Most of it is simple, but all private methods fully comply with the JSON standard.
To maintain compatibility with game consoles, exceptions are intentionally not thrown.
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;
}
Next is the Value class.
This is also not particularly difficult.
The notable point is that serialize was separated from serialize_impl.
This allows the serialization function to call itself recursively without complicating the public 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;
}
Finally, the Object class and the std::optional<> methods that did not fit on one line.
This completes the library.
All that remains is the test processing.
Since the article is getting too long, I won't show the code here.
You can check it here.
In summary, it loads JSON from memory and three JSON files, performs round-trip tests of parse → serialize → parse.
Finally, it intentionally sets an invalid JSON object to test error handling.
This test file also shows how to use the API.
That's all