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

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

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

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

Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Parse REST APIs and config files in C++ safely: nlohmann/json vs RapidJSON, contains/value/at, to_json/from_json.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, JSON, Parsing, nlohmann, RapidJSON, Serialization, REST API.