본문으로 건너뛰기
Previous
Next
C++ JSON Parsing: nlohmann/json, RapidJSON, Custom Types,

C++ JSON Parsing: nlohmann/json, RapidJSON, Custom Types,

C++ JSON Parsing: nlohmann/json, RapidJSON, Custom Types,

이 글의 핵심

JSON parsing in C++ is error-prone if done naively — type mismatches, missing keys, and parse failures all crash production services. This guide covers nlohmann/json and RapidJSON patterns that make parsing safe and readable.

The Problem with Naive JSON Parsing

C++ has no built-in JSON support, and rolling your own parser is an invitation to bugs. The common pitfalls when consuming JSON from REST APIs or config files:

  • Type mismatch: the API sends "age": "30" (string) but your code calls .get<int>()
  • Missing keys: j["optional_field"] silently inserts a null into the object
  • Parse errors: malformed JSON crashes the process if exceptions aren’t caught
  • Memory bloat: loading a 50MB JSON file into memory all at once

Two libraries dominate C++ JSON parsing: nlohmann/json (ergonomic, STL-like) and RapidJSON (fast, low memory).


Library Comparison

nlohmann/jsonRapidJSON
API styleSTL-like, intuitiveVerbose, explicit
Parse speedGoodExcellent (~2-5x faster)
MemoryHigherLower; SAX mode available
Custom typesto_json / from_jsonManual mapping
Error handlingExceptionsHasParseError() check
Header-onlyYesYes
Best forMost projectsHigh-throughput, large files

1. nlohmann/json

Install via CMake’s FetchContent or copy the single header:

#include <nlohmann/json.hpp>
using json = nlohmann::json;

Parsing a String or File

#include <nlohmann/json.hpp>
#include <fstream>
#include <stdexcept>

using json = nlohmann::json;

// Parse from string
json parseFromString(const std::string& raw) {
    try {
        return json::parse(raw);
    } catch (const json::parse_error& e) {
        throw std::runtime_error(
            "JSON parse error at byte " + std::to_string(e.byte) + ": " + e.what()
        );
    }
}

// Parse from file
json parseFromFile(const std::string& path) {
    std::ifstream file(path);
    if (!file.is_open()) {
        throw std::runtime_error("Cannot open file: " + path);
    }
    try {
        return json::parse(file);
    } catch (const json::parse_error& e) {
        throw std::runtime_error("Parse error in " + path + ": " + e.what());
    }
}

Safe Field Access

The most common mistake is using j["key"] for optional fields — it silently inserts null. Use these patterns instead:

void processApiResponse(const json& j) {
    // Required field — throws json::out_of_range if missing
    std::string id = j.at("id").get<std::string>();

    // Optional field with default
    std::string name = j.value("name", "Unknown");

    // Check before accessing
    if (j.contains("email")) {
        std::string email = j["email"].get<std::string>();
    }

    // Type checking before conversion
    if (j.contains("age") && j["age"].is_number_integer()) {
        int age = j["age"].get<int>();
    }

    // Nested object — check each level
    if (j.contains("address") && j["address"].is_object()) {
        const auto& addr = j["address"];
        std::string city = addr.value("city", "");
    }

    // Array iteration
    if (j.contains("tags") && j["tags"].is_array()) {
        for (const auto& tag : j["tags"]) {
            if (tag.is_string()) {
                std::cout << tag.get<std::string>() << '\n';
            }
        }
    }
}

Error Taxonomy

try {
    auto j = json::parse(raw_input);

    // type_error: wrong type assumed
    int x = j["count"].get<int>();  // throws if "count" is a string

} catch (const json::parse_error& e) {
    // Malformed JSON — log e.byte for the failure location
    log_error("Parse failed at byte {}: {}", e.byte, e.what());

} catch (const json::type_error& e) {
    // Type mismatch during .get<T>() or operator usage
    log_error("Type error: {}", e.what());

} catch (const json::out_of_range& e) {
    // .at("key") when key doesn't exist
    log_error("Missing field: {}", e.what());
}

2. Custom Type Serialization

For structs, define to_json and from_json as free functions:

struct User {
    std::string id;
    std::string email;
    int age{0};
    std::optional<std::string> phone;  // optional field
};

void to_json(json& j, const User& u) {
    j = json{
        {"id",    u.id},
        {"email", u.email},
        {"age",   u.age},
    };
    if (u.phone.has_value()) {
        j["phone"] = u.phone.value();
    }
}

void from_json(const json& j, User& u) {
    u.id    = j.at("id").get<std::string>();
    u.email = j.at("email").get<std::string>();
    u.age   = j.value("age", 0);

    if (j.contains("phone") && j["phone"].is_string()) {
        u.phone = j["phone"].get<std::string>();
    }
}

// Usage — automatic conversion
User user = json::parse(raw).get<User>();
json j = user;  // serialize back to JSON
std::cout << j.dump(2) << '\n';  // pretty-print with 2-space indent

Macro Shortcut for Simple Structs

struct Config {
    std::string host;
    int port{8080};
    bool debug{false};
};

// Generates to_json and from_json automatically
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Config, host, port, debug)

// Now: Config cfg = json::parse(raw).get<Config>();

3. RapidJSON

RapidJSON is faster and uses less memory, at the cost of a more verbose API:

#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
#include <rapidjson/filereadstream.h>

using namespace rapidjson;

void parseWithRapidJson(const std::string& raw) {
    Document doc;
    doc.Parse(raw.c_str());

    // Always check for parse errors
    if (doc.HasParseError()) {
        fprintf(stderr, "Parse error at offset %zu: %s\n",
            doc.GetErrorOffset(),
            GetParseError_En(doc.GetParseError()));
        return;
    }

    // Safe field access
    if (doc.HasMember("name") && doc["name"].IsString()) {
        std::string name = doc["name"].GetString();
    }

    if (doc.HasMember("count") && doc["count"].IsInt()) {
        int count = doc["count"].GetInt();
    }

    // Array
    if (doc.HasMember("items") && doc["items"].IsArray()) {
        const auto& items = doc["items"];
        for (SizeType i = 0; i < items.Size(); ++i) {
            if (items[i].IsString()) {
                printf("%s\n", items[i].GetString());
            }
        }
    }
}

File Streaming (Low Memory)

For large files, stream directly rather than loading into a std::string:

#include <rapidjson/filereadstream.h>
#include <cstdio>

void parseLargeFile(const char* path) {
    FILE* fp = fopen(path, "rb");
    if (!fp) return;

    char readBuffer[65536];
    FileReadStream is(fp, readBuffer, sizeof(readBuffer));

    Document doc;
    doc.ParseStream(is);
    fclose(fp);

    if (doc.HasParseError()) {
        fprintf(stderr, "Error: %s\n", GetParseError_En(doc.GetParseError()));
    }
}

Peak memory is roughly the size of the largest single JSON value, not the entire file.


4. Performance Comparison

Rough numbers parsing a 1 MB JSON document (environment-dependent):

LibraryParse timePeak memory
nlohmann/json~60-100ms~2× file size
RapidJSON DOM~15-30ms~1× file size
RapidJSON SAX~10-20ms~0.1× file size

Rule: use nlohmann/json unless profiling shows JSON parsing as a bottleneck. If you process megabyte-scale JSON frequently, switch to RapidJSON SAX.


Production Checklist

  • Always catch parse_error when parsing untrusted input (API responses, config files, user uploads)
  • Never use j["key"] for optional fields — it silently inserts null and corrupts your object
  • Cap input size before parsing — reject payloads over your maximum (e.g., 1MB for API requests)
  • Log the byte offset from parse_error.byte so you can diagnose truncated or malformed messages
  • Thread safety: nlohmann/json objects are not thread-safe for concurrent mutation — copy or parse per thread
  • Validate required fields with .at() so missing required data throws immediately rather than silently defaulting
  • Encode UTF-8 — both libraries expect UTF-8; validate incoming byte sequences from untrusted sources

Key Takeaways

  • nlohmann/json is the right default — readable, STL-compatible, and supports automatic custom type serialization via to_json/from_json
  • RapidJSON wins on raw performance and memory; use its SAX API for multi-MB files
  • Use value("key", default) or contains() for optional fields — never naked j["key"]
  • The error hierarchy matters: parse_error (bad JSON), type_error (wrong type), out_of_range (missing required key)
  • Define to_json/from_json or use NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE to keep struct mapping maintainable

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Parse REST APIs and config files in C++ safely: nlohmann/json vs RapidJSON, contains/value/at, to_json/from_json, parse_… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, JSON, Parsing, nlohmann, RapidJSON, Serialization, REST API 등으로 검색하시면 이 글이 도움이 됩니다.