Introduction
Almost every real API speaks JSON. In this post we’ll cover how to read and write JSON in CrowCpp — building response objects, parsing request bodies, and returning consistent error shapes. This builds on the Getting Started with Crow post.
Crow’s JSON Type: crow::json::wvalue
Crow ships its own JSON library. The main type is crow::json::wvalue — a writable JSON value that can hold objects, arrays, strings, numbers, and booleans. Return it from a route handler and Crow sets Content-Type: application/json automatically.
#include "crow.h"
int main() {
crow::SimpleApp app;
CROW_ROUTE(app, "/ping")
([]() {
crow::json::wvalue result;
result["status"] = "ok";
return result;
});
app.port(3000).run();
}
Building JSON Objects
Set fields by key, just like a map:
CROW_ROUTE(app, "/user")
([]() {
crow::json::wvalue user;
user["id"] = 1;
user["name"] = "Alice";
user["active"] = true;
return user;
});
Response: {"id":1,"name":"Alice","active":true}
Building JSON Arrays
Index into a key to build an array:
CROW_ROUTE(app, "/users")
([]() {
crow::json::wvalue resp;
resp["users"][0]["id"] = 1;
resp["users"][0]["name"] = "Alice";
resp["users"][1]["id"] = 2;
resp["users"][1]["name"] = "Bob";
return resp;
});
Or populate from a C++ vector:
CROW_ROUTE(app, "/scores")
([]() {
std::vector<int> scores = {10, 20, 30};
crow::json::wvalue resp;
for (size_t i = 0; i < scores.size(); ++i) {
resp["scores"][i] = scores[i];
}
return resp;
});
Reading JSON Request Bodies
Use crow::json::load() to parse the request body string:
CROW_ROUTE(app, "/user").methods("POST"_method)
([](const crow::request& req) {
auto body = crow::json::load(req.body);
if (!body) {
return crow::response(400, "Invalid JSON");
}
std::string name = body["name"].s();
int age = body["age"].i();
crow::json::wvalue result;
result["received_name"] = name;
result["received_age"] = age;
return crow::response(result);
});
Type Accessors
After parsing with crow::json::load(), use these methods to get typed values:
| Method | Returns |
|---|---|
.s() |
std::string |
.i() |
int64_t |
.d() |
double |
.b() |
bool |
Validating Required Fields
Always check required fields exist before accessing them:
CROW_ROUTE(app, "/user").methods("POST"_method)
([](const crow::request& req) {
auto body = crow::json::load(req.body);
if (!body || !body.has("name") || !body.has("age")) {
crow::json::wvalue err;
err["error"] = "Missing required fields: name, age";
return crow::response(400, err);
}
crow::json::wvalue result;
result["name"] = body["name"].s();
result["age"] = body["age"].i();
result["status"] = "created";
return crow::response(201, result);
});
Standard Error Responses
A helper function keeps your error shape consistent across all routes:
crow::response make_error(int code, const std::string& message) {
crow::json::wvalue err;
err["error"] = message;
err["code"] = code;
return crow::response(code, err);
}
CROW_ROUTE(app, "/item/:id").methods("GET"_method)
([](int id) {
if (id <= 0) {
return make_error(400, "ID must be positive");
}
crow::json::wvalue item;
item["id"] = id;
item["name"] = "Widget";
return crow::response(item);
});
Complete Mini-API
#include "crow.h"
#include <vector>
#include <string>
crow::response make_error(int code, const std::string& msg) {
crow::json::wvalue err;
err["error"] = msg;
return crow::response(code, err);
}
int main() {
crow::SimpleApp app;
CROW_ROUTE(app, "/ping")
([]() {
crow::json::wvalue r;
r["status"] = "ok";
return r;
});
CROW_ROUTE(app, "/users")
([]() {
crow::json::wvalue r;
r["users"][0]["id"] = 1;
r["users"][0]["name"] = "Alice";
r["users"][1]["id"] = 2;
r["users"][1]["name"] = "Bob";
return r;
});
CROW_ROUTE(app, "/users").methods("POST"_method)
([](const crow::request& req) {
auto b = crow::json::load(req.body);
if (!b || !b.has("name")) {
return make_error(400, "name is required");
}
crow::json::wvalue user;
user["id"] = 99;
user["name"] = b["name"].s();
return crow::response(201, user);
});
app.port(3000).multithreaded().run();
}
Exercises
- Add a
GET /users/:idroute that returns a user object or a 404 error whenidis greater than 10. - Add a
DELETE /users/:idroute returning{"deleted": true, "id": N}. - Extend the POST handler to accept an optional
emailfield. If present, include it in the response; if absent, default it to an empty string.
What’s Next?
- Middleware — add request logging and CORS headers to every route
- WebSockets — Crow’s built-in WebSocket support for real-time APIs
- Database — pair with SQLite via
sqlite_modern_cppfor persistent storage