C++ JSONパーサー自作シリーズ
| Part1 再帰下降 | Part2 エラー処理 | Part3 SIMD高速化 |
|---|---|---|
| ✅ | 👈 Now | - |
はじめに
前回作ったJSONパーサー、エラーメッセージがひどい。
Parse error: Expected '"' but got 'a'
どこで何がおかしいのか全然わからない。
今回は行番号・列番号付きのエラーメッセージを実装しよう。
理想のエラーメッセージ
Error at line 3, column 15:
"name": aqua,
^^^^
Expected string but found identifier 'aqua'
こんな感じで:
- 位置(行番号・列番号)がわかる
- エラー箇所が視覚的にわかる
- 何が問題で何を期待しているかわかる
位置情報の追跡
SourceLocationクラス
struct SourceLocation {
size_t offset; // 先頭からのオフセット
size_t line; // 行番号(1始まり)
size_t column; // 列番号(1始まり)
std::string to_string() const {
return "line " + std::to_string(line) + ", column " + std::to_string(column);
}
};
パーサーに位置追跡を追加
class JsonParser {
private:
std::string input_;
size_t pos_ = 0;
size_t line_ = 1;
size_t column_ = 1;
size_t line_start_ = 0; // 現在行の開始位置
SourceLocation current_location() const {
return {pos_, line_, column_};
}
char advance() {
char c = input_[pos_++];
if (c == '\n') {
line_++;
column_ = 1;
line_start_ = pos_;
} else {
column_++;
}
return c;
}
// 現在行の文字列を取得
std::string current_line_text() const {
size_t end = input_.find('\n', line_start_);
if (end == std::string::npos) {
end = input_.size();
}
return input_.substr(line_start_, end - line_start_);
}
};
エラークラス
class JsonParseError : public std::exception {
public:
std::string message;
SourceLocation location;
std::string line_text;
size_t error_length;
JsonParseError(const std::string& msg,
const SourceLocation& loc,
const std::string& line,
size_t len = 1)
: message(msg)
, location(loc)
, line_text(line)
, error_length(len)
{}
const char* what() const noexcept override {
return message.c_str();
}
std::string format() const {
std::ostringstream ss;
// ヘッダー
ss << "JSON Parse Error at " << location.to_string() << ":\n";
// 行番号付きでソースを表示
ss << " " << location.line << " | " << line_text << "\n";
// エラー位置を指し示す
ss << " " << std::string(std::to_string(location.line).size(), ' ') << " | ";
ss << std::string(location.column - 1, ' ');
ss << std::string(error_length, '^') << "\n";
// エラーメッセージ
ss << message << "\n";
return ss.str();
}
};
エラーを投げるヘルパー関数
class JsonParser {
private:
[[noreturn]] void error(const std::string& message, size_t error_len = 1) {
throw JsonParseError(
message,
current_location(),
current_line_text(),
error_len
);
}
[[noreturn]] void error_expected(const std::string& expected) {
std::string found;
if (pos_ >= input_.size()) {
found = "end of input";
} else {
found = "'" + std::string(1, peek()) + "'";
}
error("Expected " + expected + " but found " + found);
}
[[noreturn]] void error_unexpected_char() {
if (pos_ >= input_.size()) {
error("Unexpected end of input");
} else {
error("Unexpected character '" + std::string(1, peek()) + "'");
}
}
};
改良版パース関数
文字列パース
JsonValue JsonParser::parse_string() {
SourceLocation start = current_location();
if (!match('"')) {
error_expected("string");
}
std::string result;
while (true) {
if (pos_ >= input_.size()) {
throw JsonParseError(
"Unterminated string literal",
start,
current_line_text(),
pos_ - start.offset
);
}
char c = peek();
if (c == '"') {
advance();
break;
}
if (c == '\n') {
error("Newline in string literal (use \\n instead)");
}
if (c == '\\') {
advance();
if (pos_ >= input_.size()) {
error("Unterminated escape sequence");
}
char escaped = advance();
switch (escaped) {
case '"': result += '"'; break;
case '\\': result += '\\'; break;
case '/': result += '/'; break;
case 'b': result += '\b'; break;
case 'f': result += '\f'; break;
case 'n': result += '\n'; break;
case 'r': result += '\r'; break;
case 't': result += '\t'; break;
case 'u': {
if (pos_ + 4 > input_.size()) {
error("Incomplete unicode escape", 2);
}
std::string hex = input_.substr(pos_, 4);
for (char h : hex) {
if (!isxdigit(h)) {
error("Invalid unicode escape: \\u" + hex, 6);
}
}
pos_ += 4;
column_ += 4;
int codepoint = std::stoi(hex, nullptr, 16);
encode_utf8(result, codepoint);
break;
}
default:
error("Invalid escape sequence: \\" + std::string(1, escaped), 2);
}
} else if (static_cast<unsigned char>(c) < 0x20) {
error("Control character in string (use escape sequence)");
} else {
result += advance();
}
}
return JsonValue(result);
}
数値パース
JsonValue JsonParser::parse_number() {
SourceLocation start = current_location();
size_t num_start = pos_;
// 符号
if (peek() == '-') {
advance();
}
// 先頭が0の場合
if (peek() == '0') {
advance();
// 0の後に数字が続いてはいけない
if (isdigit(peek())) {
error("Leading zeros are not allowed", pos_ - num_start);
}
} else if (isdigit(peek())) {
while (isdigit(peek())) {
advance();
}
} else {
error_expected("digit");
}
// 小数部
if (peek() == '.') {
advance();
if (!isdigit(peek())) {
error_expected("digit after decimal point");
}
while (isdigit(peek())) {
advance();
}
}
// 指数部
if (peek() == 'e' || peek() == 'E') {
advance();
if (peek() == '+' || peek() == '-') {
advance();
}
if (!isdigit(peek())) {
error_expected("digit in exponent");
}
while (isdigit(peek())) {
advance();
}
}
// 数値に変換
std::string num_str = input_.substr(num_start, pos_ - num_start);
try {
double value = std::stod(num_str);
// オーバーフローチェック
if (std::isinf(value)) {
throw JsonParseError(
"Number overflow: " + num_str,
start,
current_line_text(),
num_str.size()
);
}
return JsonValue(value);
} catch (const std::out_of_range&) {
throw JsonParseError(
"Number out of range: " + num_str,
start,
current_line_text(),
num_str.size()
);
}
}
オブジェクトパース
JsonValue JsonParser::parse_object() {
SourceLocation start = current_location();
expect('{');
skip_whitespace();
JsonObject obj;
if (peek() != '}') {
while (true) {
skip_whitespace();
// キーは文字列でなければならない
if (peek() != '"') {
if (isalpha(peek())) {
// よくあるミス:クォートなしのキー
std::string word = parse_identifier();
error("Object keys must be strings. Did you mean \"" + word + "\"?",
word.size());
}
error_expected("string key");
}
std::string key = parse_string().as_string();
// 重複キーの警告
if (obj.find(key) != obj.end()) {
// 警告として扱う(エラーにはしない)
std::cerr << "Warning: Duplicate key '" << key << "' at "
<< current_location().to_string() << "\n";
}
skip_whitespace();
if (!match(':')) {
error_expected("':' after object key");
}
skip_whitespace();
obj[key] = parse_value();
skip_whitespace();
if (match(',')) {
skip_whitespace();
// 末尾のカンマチェック
if (peek() == '}') {
error("Trailing comma in object is not allowed");
}
} else if (peek() == '}') {
break;
} else {
error_expected("',' or '}'");
}
}
}
expect('}');
return JsonValue(obj);
}
// 識別子を読み取る(エラーメッセージ用)
std::string JsonParser::parse_identifier() {
std::string result;
while (isalnum(peek()) || peek() == '_') {
result += advance();
}
return result;
}
コンテキスト情報の追加
深いネストでエラーが起きたとき、どこでエラーになったかわかりにくい。
class JsonParser {
private:
std::vector<std::string> context_stack_;
void push_context(const std::string& ctx) {
context_stack_.push_back(ctx);
}
void pop_context() {
context_stack_.pop_back();
}
std::string context_string() const {
if (context_stack_.empty()) return "";
std::string result = "In ";
for (size_t i = 0; i < context_stack_.size(); ++i) {
if (i > 0) result += " -> ";
result += context_stack_[i];
}
return result + ":\n";
}
[[noreturn]] void error(const std::string& message, size_t error_len = 1) {
throw JsonParseError(
context_string() + message,
current_location(),
current_line_text(),
error_len
);
}
};
// 使い方
JsonValue JsonParser::parse_object() {
push_context("object");
// ... パース処理
pop_context();
return result;
}
JsonValue JsonParser::parse_array() {
push_context("array[" + std::to_string(index) + "]");
// ... パース処理
pop_context();
return result;
}
出力例:
In object -> array[2] -> object:
Expected string but found number
RAIIでコンテキスト管理
class ContextGuard {
public:
ContextGuard(JsonParser& parser, const std::string& ctx)
: parser_(parser)
{
parser_.push_context(ctx);
}
~ContextGuard() {
parser_.pop_context();
}
private:
JsonParser& parser_;
};
// 使い方
JsonValue JsonParser::parse_object() {
ContextGuard guard(*this, "object");
// 例外が投げられても自動的にpop_context()される
// ...
}
サジェスト機能
よくある間違いに対して修正案を提示。
void JsonParser::suggest_fix(const std::string& input, size_t pos) {
// true/falseのタイポ
static const std::map<std::string, std::string> typo_fixes = {
{"True", "true"},
{"TRUE", "true"},
{"False", "false"},
{"FALSE", "false"},
{"Null", "null"},
{"NULL", "null"},
{"None", "null"}, // Pythonユーザー向け
{"nil", "null"}, // Rubyユーザー向け
{"undefined", "null"}, // JSユーザー向け
};
for (const auto& [typo, fix] : typo_fixes) {
if (input.substr(pos, typo.size()) == typo) {
error("Invalid value '" + typo + "'. Did you mean '" + fix + "'?",
typo.size());
}
}
}
バリデーション
パース後にスキーマに沿っているか検証。
class JsonValidator {
public:
struct ValidationError {
std::string path;
std::string message;
};
std::vector<ValidationError> validate(const JsonValue& value,
const JsonValue& schema) {
errors_.clear();
validate_impl(value, schema, "");
return errors_;
}
private:
std::vector<ValidationError> errors_;
void validate_impl(const JsonValue& value,
const JsonValue& schema,
const std::string& path) {
if (!schema.is_object()) return;
const auto& s = schema.as_object();
// type チェック
if (s.count("type")) {
std::string expected_type = s.at("type").as_string();
std::string actual_type = get_type_name(value);
if (expected_type != actual_type) {
errors_.push_back({
path.empty() ? "/" : path,
"Expected type '" + expected_type +
"' but got '" + actual_type + "'"
});
}
}
// required チェック
if (s.count("required") && value.is_object()) {
for (const auto& req : s.at("required").as_array()) {
std::string key = req.as_string();
if (!value.as_object().count(key)) {
errors_.push_back({
path + "/" + key,
"Missing required property '" + key + "'"
});
}
}
}
// properties チェック
if (s.count("properties") && value.is_object()) {
const auto& props = s.at("properties").as_object();
for (const auto& [key, val] : value.as_object()) {
if (props.count(key)) {
validate_impl(val, props.at(key), path + "/" + key);
}
}
}
// items チェック
if (s.count("items") && value.is_array()) {
const auto& item_schema = s.at("items");
size_t i = 0;
for (const auto& item : value.as_array()) {
validate_impl(item, item_schema,
path + "[" + std::to_string(i) + "]");
++i;
}
}
}
std::string get_type_name(const JsonValue& v) {
if (v.is_null()) return "null";
if (v.is_bool()) return "boolean";
if (v.is_number()) return "number";
if (v.is_string()) return "string";
if (v.is_array()) return "array";
if (v.is_object()) return "object";
return "unknown";
}
};
使用例
int main() {
JsonParser parser;
std::string json = R"({
"name": Aqua,
"age": 15,
"skills": ["C++", "Rust", ]
})";
try {
auto value = parser.parse(json);
} catch (const JsonParseError& e) {
std::cerr << e.format();
return 1;
}
return 0;
}
出力:
JSON Parse Error at line 2, column 17:
2 | "name": Aqua,
| ^^^^
Object keys must be strings. Did you mean "Aqua"?
まとめ
| トピック | ポイント |
|---|---|
| 位置追跡 | 行番号・列番号を記録 |
| エラー表示 | ソースコードと矢印で視覚化 |
| コンテキスト | ネストした場所を表示 |
| サジェスト | よくあるミスに修正案 |
| バリデーション | スキーマに沿っているか検証 |
次回はSIMDで高速化して、大量のJSONを爆速でパースできるようにするよ。
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!