はじめに
そんなに高機能でなくてよいので、JSONファイルの書込み機能が欲しくなった。
読み込み機能同様、JSON用のライブラリは使わずに実現したい。
JSONファイル書込み機能
#pragma once
#include <cctype>
#include <cstddef>
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <type_traits>
#include <unordered_map>
#include <variant>
#include <vector>
namespace SimpleJson {
/// @brief JSONの型を表す列挙型
enum class Type {
Null,
Bool,
Number,
String,
Array,
Object
};
/// @brief JSON値を表すクラス
class JsonValue {
public:
// JSON値の型定義
Type type = Type::Null;
// データ保持構造(共用体定義)
using Array = std::vector<JsonValue>;
using Object = std::vector<std::pair<std::string, JsonValue>>;
std::variant<nullptr_t, bool, double, std::string, Array, Object> data;
// デフォルトコンストラクタと型指定コンストラクタ
JsonValue() : type(Type::Null), data(nullptr) {}
JsonValue(Type t) : type(t) {
if (t == Type::Array) data = Array{};
else if (t == Type::Object) data = Object{};
}
// JSONの型ごとの値コンストラクタ
JsonValue(nullptr_t) : type(Type::Null), data(nullptr) {}
JsonValue(bool v) : type(Type::Bool), data(v) {}
JsonValue(double v) : type(Type::Number), data(v) {}
JsonValue(int v) : type(Type::Number), data(static_cast<double>(v)) {}
JsonValue(long long v) : type(Type::Number), data(static_cast<double>(v)) {}
JsonValue(const char* v) : type(Type::String), data(std::string(v)) {}
JsonValue(const std::string& v) : type(Type::String), data(v) {}
// --- 値の取得 (Reader機能) ---
bool as_bool() const { return std::get<bool>(data); }
double as_number() const { return std::get<double>(data); }
const std::string& as_string() const { return std::get<std::string>(data); }
const Array& as_array() const { return std::get<Array>(data); }
const Object& as_object() const { return std::get<Object>(data); }
// 読み込み用ブラケット演算子
const JsonValue& operator[](const std::string& key) const {
if (type != Type::Object) throw std::runtime_error("Not a JSON Object");
const auto& obj = as_object();
for (const auto& kv : obj) {
if (kv.first == key) return kv.second;
}
throw std::out_of_range("Key not found: " + key);
}
const JsonValue& operator[](size_t index) const {
if (type != Type::Array) throw std::runtime_error("Not a JSON Array");
return as_array().at(index);
}
// --- 値の構築・追加 (Writer機能) ---
// オブジェクトへのキー・値の追加
void add(const std::string& key, JsonValue val) {
if (type != Type::Object) type = Type::Object, data = Object{};
std::get<Object>(data).push_back({key, std::move(val)});
}
// 配列への要素追加
void push_back(JsonValue val) {
if (type != Type::Array) type = Type::Array, data = Array{};
std::get<Array>(data).push_back(std::move(val));
}
// エスケープ処理
static std::string escape_string(const std::string& src) {
std::ostringstream ss;
for (char c : src) {
switch (c) {
case '\"': ss << "\\\""; break;
case '\\': ss << "\\\\"; break;
case '\b': ss << "\\b"; break;
case '\f': ss << "\\f"; break;
case '\n': ss << "\\n"; break;
case '\r': ss << "\\r"; break;
case '\t': ss << "\\t"; break;
default: ss << c; break;
}
}
return ss.str();
}
// シリアライズ(JSON文字列変換)
std::string to_string(int indentLevel = 0, int indentStep = 2) const {
std::string indent(indentLevel * indentStep, ' ');
std::string child_indent((indentLevel + 1) * indentStep, ' ');
std::ostringstream ss;
switch (type) {
case Type::Null: return "null";
case Type::Bool: return as_bool() ? "true" : "false";
case Type::Number: {
double num = as_number();
if (num == static_cast<long long>(num)) {
return std::to_string(static_cast<long long>(num));
}
return std::to_string(num);
}
case Type::String: return "\"" + escape_string(as_string()) + "\"";
case Type::Array: {
const auto& arr = as_array();
if (arr.empty()) return "[]";
ss << "[\n";
for (size_t i = 0; i < arr.size(); ++i) {
ss << child_indent << arr[i].to_string(indentLevel + 1, indentStep);
if (i + 1 < arr.size()) ss << ",";
ss << "\n";
}
ss << indent << "]";
return ss.str();
}
case Type::Object: {
const auto& obj = as_object();
if (obj.empty()) return "{}";
ss << "{\n";
for (size_t i = 0; i < obj.size(); ++i) {
ss << child_indent << "\"" << escape_string(obj[i].first) << "\": "
<< obj[i].second.to_string(indentLevel + 1, indentStep);
if (i + 1 < obj.size()) ss << ",";
ss << "\n";
}
ss << indent << "}";
return ss.str();
}
}
return "null";
}
// ファイル書き出し
bool write(const std::string& filepath, int indentStep = 2) const {
std::ofstream ofs(filepath);
if (!ofs.is_open()) return false;
ofs << to_string(0, indentStep) << std::endl;
return true;
}
};
// -------------------------------------------------------------
// JSON パーサー (Reader)
// -------------------------------------------------------------
class JsonReader {
public:
static JsonValue parse(const std::string& json_str) {
size_t index = 0;
skip_whitespace(json_str, index);
JsonValue result = parse_value(json_str, index);
return result;
}
static JsonValue parse_file(const std::string& filepath) {
std::ifstream file(filepath);
if (!file.is_open()) {
throw std::runtime_error("ファイルを開けませんでした: " + filepath);
}
std::stringstream buffer;
buffer << file.rdbuf();
return parse(buffer.str());
}
private:
static void skip_whitespace(const std::string& str, size_t& i) {
while (i < str.size() && (str[i] == ' ' || str[i] == '\t' || str[i] == '\n' || str[i] == '\r')) {
i++;
}
}
static JsonValue parse_value(const std::string& str, size_t& i) {
skip_whitespace(str, i);
if (i >= str.size()) throw std::runtime_error("予期せぬ構文終了です");
char c = str[i];
if (c == '{') return parse_object(str, i);
if (c == '[') return parse_array(str, i);
if (c == '"') return parse_string(str, i);
if (c == 't' || c == 'f') return parse_bool(str, i);
if (c == 'n') return parse_null(str, i);
if (c == '-' || std::isdigit(c)) return parse_number(str, i);
throw std::runtime_error(std::string("無効な文字が見つかりました: ") + c);
}
static JsonValue parse_string(const std::string& str, size_t& i) {
i++; // '"' をスキップ
std::string res;
while (i < str.size() && str[i] != '"') {
if (str[i] == '\\' && i + 1 < str.size()) {
i++;
if (str[i] == 'n') res += '\n';
else if (str[i] == 't') res += '\t';
else res += str[i];
} else {
res += str[i];
}
i++;
}
i++; // '"' をスキップ
return JsonValue(res);
}
static JsonValue parse_number(const std::string& str, size_t& i) {
size_t start = i;
if (str[i] == '-') i++;
while (i < str.size() && (std::isdigit(str[i]) || str[i] == '.')) {
i++;
}
double val = std::stod(str.substr(start, i - start));
return JsonValue(val);
}
static JsonValue parse_object(const std::string& str, size_t& i) {
i++; // '{' をスキップ
JsonValue obj(Type::Object);
skip_whitespace(str, i);
if (str[i] == '}') { i++; return obj; }
while (i < str.size()) {
skip_whitespace(str, i);
std::string key = parse_string(str, i).as_string();
skip_whitespace(str, i);
if (str[i] != ':') throw std::runtime_error("':' が見つかりません");
i++;
JsonValue val = parse_value(str, i);
obj.add(key, val);
skip_whitespace(str, i);
if (str[i] == '}') { i++; break; }
if (str[i] == ',') { i++; continue; }
throw std::runtime_error("オブジェクトの区切りが無効です");
}
return obj;
}
static JsonValue parse_array(const std::string& str, size_t& i) {
i++; // '[' をスキップ
JsonValue arr(Type::Array);
skip_whitespace(str, i);
if (str[i] == ']') { i++; return arr; }
while (i < str.size()) {
arr.push_back(parse_value(str, i));
skip_whitespace(str, i);
if (str[i] == ']') { i++; break; }
if (str[i] == ',') { i++; continue; }
throw std::runtime_error("配列の区切りが無効です");
}
return arr;
}
static JsonValue parse_bool(const std::string& str, size_t& i) {
if (str.compare(i, 4, "true") == 0) { i += 4; return JsonValue(true); }
if (str.compare(i, 5, "false") == 0) { i += 5; return JsonValue(false); }
throw std::runtime_error("ブール値のパースエラー");
}
static JsonValue parse_null(const std::string& str, size_t& i) {
if (str.compare(i, 4, "null") == 0) { i += 4; return JsonValue(nullptr); }
throw std::runtime_error("nullのパースエラー");
}
};
} // namespace SimpleJson
JSON書込みクラスを使ってみる
書込み構造を決める
構造体にを作っておいたほうが、後々のメンテナンスがしやすいと思うので作る。
サンプルとして、クラスごとの各生徒のテストの点数を記録する構造体を作ってみる。
// 科目ごとの点数
struct SubjectScore {
int japanese; // 国語
int mathematics; // 数学
int english; // 英語
};
// 生徒ごとの情報
struct StudentScore {
std::string name; // 生徒名
SubjectScore subject; // 各科目の点数(構造体のネスト)
};
// クラス全体の成績集計データ
struct TestScoreTabulation {
std::string className; // クラス名 (例: "3-A")
float averageScore; // クラス平均点
std::vector<StudentScore> students; // 各生徒の点数リスト(配列)
};
サンプルコード
#include <iostream>
#include <vector>
#include <string>
#include "JsonWriter.hpp"
using namespace SimpleJson;
// === 構造体の定義 ===
// 科目ごとの点数
struct SubjectScore {
int japanese; // 国語
int mathematics; // 数学
int english; // 英語
};
// 生徒ごとの情報
struct StudentScore {
std::string name; // 生徒名
SubjectScore subject; // 各科目の点数(階層)
};
// クラス全体の成績集計データ
struct TestScoreTabulation {
std::string className; // クラス名 (例: "3-A")
float averageScore; // クラス平均点
std::vector<StudentScore> students; // 各生徒の点数リスト(配列)
};
// === 成績集計データを JSON ファイルに出力する関数 ===
bool export_test_scores(const std::string& filepath,
const TestScoreTabulation& data) {
// 1.ルートオブジェクトの作成
JsonValue root(Type::Object);
// 2. ルート直下の基本情報
root.add("class_name", data.className);
root.add("average_score", data.averageScore);
// 3. 生徒配列を作成
JsonValue studentArray(Type::Array);
for (const auto& student : data.students) {
// 生徒オブジェクトの作成
JsonValue studentObj(Type::Object);
studentObj.add("name", student.name);
// 科目点数オブジェクト(子オブジェクト)の作成
JsonValue scoresObj(Type::Object);
scoresObj.add("japanese", student.subject.japanese);
scoresObj.add("mathematics", student.subject.mathematics);
scoresObj.add("english", student.subject.english);
// 生徒オブジェクトに科目オブジェクトを登録
studentObj.add("scores", scoresObj);
// 配列に生徒を追加
studentArray.push_back(studentObj);
}
// 4. ルートに生徒配列を登録
root.add("students", studentArray);
// 5. ファイル保存
return root.write(filepath);
}
int main()
{
// 生徒3名分のデータを用意
TestScoreTabulation tab;
tab.className = "3-A";
tab.averageScore = 78.5f;
tab.students = {
{ "Sato", { 85, 90, 78 } }, // 佐藤くん
{ "Suzuki", { 70, 65, 82 } }, // 鈴木さん
{ "Takahashi", { 95, 88, 92 } } // 高橋くん
};
// 出力実行
std::string filename = "./class_3a_scores.json";
if (export_test_scores(filename, tab)) {
std::cout << "Successfully saved to " << filename << std::endl;
}
return 0;
}
出力結果
{
"class_name": "3-A",
"average_score": 78.500000,
"students": [
{
"name": "Sato",
"scores": {
"japanese": 85,
"mathematics": 90,
"english": 78
}
},
{
"name": "Suzuki",
"scores": {
"japanese": 70,
"mathematics": 65,
"english": 82
}
},
{
"name": "Takahashi",
"scores": {
"japanese": 95,
"mathematics": 88,
"english": 92
}
}
]
}
処理説明
1.ルートオブジェクトの作成
// 1.ルートオブジェクトの作成
JsonValue root(Type::Object);
JsonValue(Type::Object) を使用して、JSONのルートオブジェクトを生成します。
以降rootへ書き出したい項目や配列を登録してきます。
2.新規項目および値を登録する
// 2. ルート直下の基本情報
root.add("class_name", data.className);
root.add("average_score", data.averageScore);
上記で作成した root オブジェクトの add()メソッドを使用して項目と値を登録します。
data.className、data.averageScoreのデータ型は、それぞれ std:::string と floatのため、
下記の add() が呼ばれる。ただし、第2引数が JsonValue val で型違いのため、
C++コンパイラが、 JsonValue(std::string型)のコンストラクタを見つけ、暗黙の型変換をして処理してくれます。
また、JsonValue(float型)のコンストラクタは未定義ですが、コンパイラが精度を落とさない型変換としてJsonValue(double型)のコンストラクタを選択してくれる(気持ち悪ければ、追加してもよいと思う)。
// JSONの型ごとの値コンストラクタ
JsonValue(const std::string& v) : type(Type::String), data(v) {}
JsonValue(double v) : type(Type::Number), data(v) {}
// オブジェクトへのキー・値の追加
void add(const std::string& key, JsonValue val) {
if (type != Type::Object) type = Type::Object, data = Object{};
std::get<Object>(data).push_back({key, std::move(val)});
}
3.配列の登録
// 3. 生徒配列を作成
JsonValue studentArray(Type::Array);
for (const auto& student : data.students) {
// 生徒オブジェクトの作成
JsonValue studentObj(Type::Object);
studentObj.add("name", student.name);
// ~~~ (省略) ~~~
// 配列に生徒を追加
studentArray.push_back(studentObj);
}
// 4. ルートに生徒配列を登録
root.add("students", studentArray);
JsonValue(Type::Array) を使用して、配列型のオブジェクトを作成します。
そして配列に登録する実体が必要なため、JsonValue(Type::Object) でオブジェクトを作成します。
オブジェクトへの項目登録が終わったら、配列型オブジェクトの push_back()メソッドを使い、必要な数だけオブジェクトを登録する。
使用用途は今のところないため push_front()などは未作成です。
配列オブジェクトを root へ登録するには、項目追加と同様 add() メソッドで行います。
4.JSON書き込み
// 5. ファイル保存
return root.write(filepath);
root オブジェクトの write()メソッドを呼び出すことで JSONファイルが作成されます。
JSONファイルへの項目の書き出し順は、root オブジェクトへの登録順となります。