JSON Wrapper
Overview
The R-Type project includes a custom JSON wrapper (rtype::Json) that provides a safe, type-checked interface around the nlohmann::json library. This wrapper aims to simplify JSON parsing and manipulation while providing clear error messages and better exception handling.
Location
- Header:
shared/include/json/Json.hpp - Implementation:
shared/src/json/Json.cpp - Tests:
tests/shared/JsonTests.cpp
Purpose
The JSON wrapper abstracts the underlying JSON library implementation and provides:
- Type safety: Explicit type checking with clear error messages
- Custom exceptions: Dedicated exception types for different error scenarios
- Simplified API: Clean, intuitive methods for common JSON operations
- Default values: Easy handling of optional fields with fallback values
Exception Types
The wrapper defines three custom exception types:
JsonParseError
Thrown when JSON parsing fails (invalid JSON string).
JsonKeyError
Thrown when trying to access a non-existent key.
JsonTypeError
Thrown when type conversion fails or when an operation is performed on the wrong JSON type.
Core API
Creating JSON Objects
Parse from string
Create empty object
Create empty array
Reading Values
Get value with type checking
Json json = Json::parse(jsonStr);
std::string name = json.getValue<std::string>("name");
int health = json.getValue<int>("health");
Throws JsonKeyError if key doesn't exist, or JsonTypeError if type doesn't match.
Get value with default fallback
int score = json.getValue<int>("score", 0); // Returns 0 if key doesn't exist
std::string team = json.getValue<std::string>("team", "red");
Get value directly (for non-object JSON)
Check if key exists
Setting Values
Json player = Json::object();
player.setValue("name", "Player1");
player.setValue("score", 1000);
player.setValue("active", true);
Array Operations
Create and populate array
Json enemies = Json::array();
Json enemy1 = Json::object();
enemy1.setValue("type", "zombie");
enemy1.setValue("health", 50);
Json enemy2 = Json::object();
enemy2.setValue("type", "skeleton");
enemy2.setValue("health", 30);
enemies.pushBack(enemy1);
enemies.pushBack(enemy2);
Access array elements
Array size
Type Checking
if (json.isObject()) { /* ... */ }
if (json.isArray()) { /* ... */ }
if (json.isString()) { /* ... */ }
if (json.isBoolean()) { /* ... */ }
if (json.isNumber()) { /* ... */ }
if (json.isNumberInteger()) { /* ... */ }
if (json.isNumberUnsigned()) { /* ... */ }
Object Inspection
Get all keys
std::vector<std::string> keys = json.getKeys();
for (const auto& key : keys) {
std::cout << key << std::endl;
}
Check if empty
Nested Access
std::string jsonStr = R"({"config": {"graphics": {"resolution": "1920x1080"}}})";
Json json = Json::parse(jsonStr);
Json config = json["config"];
Json graphics = config["graphics"];
std::string resolution = graphics.getValue<std::string>("resolution");
Serialization
Compact output
Pretty-printed output
std::string pretty = json.dump(2); // Indented with 2 spaces
std::string pretty4 = json.dump(4); // Indented with 4 spaces
Advanced Usage
Access underlying nlohmann::json
If you need direct access to the underlying nlohmann::json object:
const nlohmann::json& internal = json.getInternal();
nlohmann::json& mutableInternal = json.getInternal();
This is useful when integrating with code that still uses nlohmann::json directly.
Usage Examples
Parsing a configuration file
try {
std::ifstream file("config.json");
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
Json config = Json::parse(content);
std::string serverIP = config.getValue<std::string>("server_ip", "localhost");
int port = config.getValue<int>("port", 8080);
bool debugMode = config.getValue<bool>("debug", false);
// Use configuration...
} catch (const JsonParseError& e) {
std::cerr << "Config parse error: " << e.what() << std::endl;
} catch (const JsonTypeError& e) {
std::cerr << "Config type error: " << e.what() << std::endl;
}
Building a JSON response
Json response = Json::object();
response.setValue("status", "success");
response.setValue("timestamp", std::time(nullptr));
Json data = Json::object();
data.setValue("playerId", 42);
data.setValue("score", 15000);
response.setValue("data", data.getInternal());
std::string jsonString = response.dump(2);
// Send jsonString over network...
Processing a list of entities
std::string entitiesJson = R"({
"entities": [
{"id": 1, "type": "player", "x": 100, "y": 200},
{"id": 2, "type": "enemy", "x": 300, "y": 400}
]
})";
Json root = Json::parse(entitiesJson);
Json entities = root["entities"];
for (std::size_t i = 0; i < entities.size(); ++i) {
Json entity = entities[i];
int id = entity.getValue<int>("id");
std::string type = entity.getValue<std::string>("type");
int x = entity.getValue<int>("x");
int y = entity.getValue<int>("y");
// Process entity...
}
Migration Guide
If you have code using nlohmann::json directly, here's how to migrate:
Before (nlohmann::json)
nlohmann::json json = nlohmann::json::parse(str);
std::string value = json["key"];
int number = json.value("number", 0);
After (rtype::Json)
Json json = Json::parse(str);
std::string value = json.getValue<std::string>("key");
int number = json.getValue<int>("number", 0);
Error Handling Best Practices
Always wrap JSON operations in try-catch blocks:
try {
Json config = Json::parse(configString);
// Process config...
} catch (const JsonParseError& e) {
// Handle parse errors (invalid JSON)
std::cerr << "Parse error: " << e.what() << std::endl;
} catch (const JsonKeyError& e) {
// Handle missing keys
std::cerr << "Missing key: " << e.what() << std::endl;
} catch (const JsonTypeError& e) {
// Handle type mismatches
std::cerr << "Type error: " << e.what() << std::endl;
}
Testing
The JSON wrapper includes comprehensive unit tests in tests/shared/JsonTests.cpp. Run these tests to verify the wrapper's behavior:
Implementation Notes
- The wrapper uses
nlohmann::jsoninternally for actual JSON processing - All methods are designed to be exception-safe
- The
[[nodiscard]]attribute is used on query methods to prevent ignoring return values - Default constructors create empty objects (not null)
- Array indexing is bounds-checked and throws
std::out_of_rangefor invalid indices