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/json | RapidJSON | |
|---|---|---|
| API style | STL-like, intuitive | Verbose, explicit |
| Parse speed | Good | Excellent (~2-5x faster) |
| Memory | Higher | Lower; SAX mode available |
| Custom types | to_json / from_json | Manual mapping |
| Error handling | Exceptions | HasParseError() check |
| Header-only | Yes | Yes |
| Best for | Most projects | High-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):
| Library | Parse time | Peak 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_errorwhen 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.byteso 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)orcontains()for optional fields — never nakedj["key"] - The error hierarchy matters:
parse_error(bad JSON),type_error(wrong type),out_of_range(missing required key) - Define
to_json/from_jsonor useNLOHMANN_DEFINE_TYPE_NON_INTRUSIVEto 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 처리 | nlohmann/json으로 파싱과 생성하기 [#27-2]
- C++ REST API 클라이언트 완벽 가이드 | CRUD·인증·에러 처리·프로덕션 패턴 [#21-3]
- C++ REST API 서버 완벽 가이드 | Beast 라우팅·JSON·미들웨어 [#31-2]
이 글에서 다루는 키워드 (관련 검색어)
C++, JSON, Parsing, nlohmann, RapidJSON, Serialization, REST API 등으로 검색하시면 이 글이 도움이 됩니다.