CrowCpp JSON: Parse Requests and Build Responses

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

  1. Add a GET /users/:id route that returns a user object or a 404 error when id is greater than 10.
  2. Add a DELETE /users/:id route returning {"deleted": true, "id": N}.
  3. Extend the POST handler to accept an optional email field. 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_cpp for persistent storage

Leave a Comment

Your email address will not be published. Required fields are marked *