The JSON-RPC 2.0 library now supports automatic serialization and deserialization of C++ types! Instead of manually converting between JSON and C++ types, you can now register handlers that work directly with C++ types.
- Type Safety: Compile-time type checking
- Less Boilerplate: No manual JSON conversion
- Better Error Messages: Automatic invalid params detection
- Custom Types: Works with any type that can be serialized/deserialized by nlohmann::json
dispatcher disp;
disp.add("add", [](const json ¶ms) -> json {
int a = params["a"].get<int>();
int b = params["b"].get<int>();
return a + b;
});struct AddParams {
int a;
int b;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AddParams, a, b)
dispatcher disp;
disp.add_typed<AddParams, int>("add", [](AddParams params) {
return params.a + params.b;
});// int -> int
disp.add_typed<int, int>("double", [](int x) {
return x * 2;
});
// string -> string
disp.add_typed<std::string, std::string>("greet", [](std::string name) {
return "Hello, " + name + "!";
});struct Point {
double x;
double y;
};
// Define serialization
namespace nlohmann {
template <>
struct adl_serializer<Point> {
static void to_json(json &j, const Point &p) {
j = json{{"x", p.x}, {"y", p.y}};
}
static void from_json(const json &j, Point &p) {
j.at("x").get_to(p.x);
j.at("y").get_to(p.y);
}
};
}
// Use it
disp.add_typed<Point, Point>("move_point", [](Point p) {
p.x += 10;
p.y += 20;
return p;
});// vector<int> -> vector<int>
disp.add_typed<std::vector<int>, std::vector<int>>("double_all",
[](std::vector<int> nums) {
for (auto &n : nums) n *= 2;
return nums;
}
);int counter = 0;
// int -> void (notification-style)
disp.add_typed<int, void>("increment", [&](int x) {
counter += x;
});// void -> int
disp.add_no_params<int>("get_random", []() {
return rand() % 100;
});struct User {
std::string name;
int age;
std::vector<std::string> hobbies;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(User, name, age, hobbies)
disp.add_typed<User, User>("update_user", [](User u) {
u.age += 1; // Birthday!
return u;
});The library automatically handles JSON-RPC 2.0 parameter format requirements:
- Single parameters are wrapped in an array:
[5] - Struct parameters are serialized as objects:
{"a": 1, "b": 2} - Array parameters are passed as-is:
[1, 2, 3]
// Single int parameter
{
"jsonrpc": "2.0",
"method": "double",
"params": [5],
"id": 1
}
// Struct parameter
{
"jsonrpc": "2.0",
"method": "add",
"params": [{"a": 10, "b": 20}],
"id": 2
}
// Array parameter
{
"jsonrpc": "2.0",
"method": "sum",
"params": [[1, 2, 3, 4, 5]],
"id": 3
}You can also use typed methods on the client side:
endpoint ep(sender);
// Send typed request
ep.send_request_typed<std::string, std::string>(
"greet",
std::string("Alice"),
[](std::string result) {
std::cout << "Got: " << result << "\n";
},
[](const json &error) {
std::cerr << "Error: " << error["message"] << "\n";
}
);
// Send typed notification
ep.send_notification_typed<int>("log_value", 42);Invalid parameters are automatically detected:
disp.add_typed<int, int>("need_int", [](int x) {
return x * 2;
});
// If client sends: {"jsonrpc": "2.0", "method": "need_int", "params": ["not_a_number"], "id": 1}
// Response: {
// "jsonrpc": "2.0",
// "id": 1,
// "error": {
// "code": -32602,
// "message": "Invalid params",
// "data": {"what": "type must be number, but is string"}
// }
// }You can use both styles in the same dispatcher:
dispatcher disp;
// Raw JSON handler
disp.add("raw_method", [](const json ¶ms) -> json {
return params["value"].get<int>() * 3;
});
// Typed handler
disp.add_typed<int, int>("typed_method", [](int x) {
return x * 3;
});
// Both work!#include "include/jsonrpc.hpp"
#include <iostream>
using namespace pooriayousefi;
struct Calculator {
double memory = 0.0;
};
struct CalcRequest {
std::string operation;
double a;
double b;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(CalcRequest, operation, a, b)
int main() {
Calculator calc;
dispatcher disp;
// Typed method: CalcRequest -> double
disp.add_typed<CalcRequest, double>("calculate",
[&](CalcRequest req) -> double {
if (req.operation == "add") return req.a + req.b;
if (req.operation == "sub") return req.a - req.b;
if (req.operation == "mul") return req.a * req.b;
if (req.operation == "div") {
if (req.b == 0) {
throw_rpc_error(error{-32000, "Division by zero"});
}
return req.a / req.b;
}
throw_rpc_error(error{-32001, "Unknown operation"});
});
// Typed method: double -> void (store in memory)
disp.add_typed<double, void>("store", [&](double value) {
calc.memory = value;
});
// No params: return stored value
disp.add_no_params<double>("recall", [&]() {
return calc.memory;
});
// Test it
json req1 = make_request(1, "calculate",
json::array({json{{"operation", "add"}, {"a", 10.0}, {"b", 20.0}}}));
auto resp1 = disp.handle(req1);
std::cout << "Result: " << (*resp1)["result"] << "\n"; // 30.0
json req2 = make_request(2, "store", json::array({42.5}));
disp.handle(req2);
json req3 = make_request(3, "recall", json::object());
auto resp3 = disp.handle(req3);
std::cout << "Memory: " << (*resp3)["result"] << "\n"; // 42.5
return 0;
}add_typed<ParamsT, ResultT>(method, handler)- Register typed handleradd_no_params<ResultT>(method, handler)- Register handler with no parametersadd(method, handler)- Register raw JSON handler (original method)
add_typed<ParamsT, ResultT>(method, handler)- Server-side typed handleradd_no_params<ResultT>(method, handler)- Server-side no-params handlersend_request_typed<ParamsT, ResultT>(method, params, on_result, on_error)- Client-side typed requestsend_notification_typed<ParamsT>(method, params)- Client-side typed notification
- Parameter Format: Single parameters are wrapped in arrays for JSON-RPC 2.0 compliance
- Type Safety: Invalid params trigger
-32602error with details - Performance: Zero overhead - templates resolve at compile time
- Compatibility: Works alongside existing raw JSON handlers
- Flexibility: Supports any type that nlohmann::json can serialize/deserialize
To migrate existing code:
- Identify handlers that work with structured data
- Define C++ structs for your parameter types
- Add serialization using
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVEor customadl_serializer - Replace
add(...)withadd_typed<ParamsT, ResultT>(...) - Simplify handler logic by removing manual
.get<>()calls
Before:
disp.add("create_user", [](const json ¶ms) -> json {
std::string name = params["name"].get<std::string>();
int age = params["age"].get<int>();
User u{name, age, {}};
users.push_back(u);
return json{{"id", users.size() - 1}};
});After:
struct CreateUserParams {
std::string name;
int age;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(CreateUserParams, name, age)
disp.add_typed<CreateUserParams, int>("create_user",
[](CreateUserParams params) {
User u{params.name, params.age, {}};
users.push_back(u);
return users.size() - 1;
}
);Much cleaner! 🎉