nlohmann/json
A complete, runnable example of teaching Burl a body type it has never heard of,
extracted from example/nlohmann_json.cpp.
Burl ships conversions for strings, Boost.JSON, forms, and files, but the same
mechanism is open: a pair of tag_invoke overloads, found by argument-dependent
lookup, makes any type a first-class body in both directions. Here that type is
nlohmann::json.
See Extending for the concepts this example puts to work.
Sending: body_from_tag
To send a value with body, provide a tag_invoke
overload taking body_from_tag<T>. It returns an any_request_body, a
type-erased wrapper around any object satisfying the RequestBody concept.
Here the document is serialized up front with dump(), so the body owns the
text and can report a known Content-Length. write then hands that buffer to
the sink in a single call:
burl::any_request_body
tag_invoke(burl::body_from_tag<nlohmann::json>, const nlohmann::json& value)
{
class json_body
{
std::string text_;
public:
explicit json_body(const nlohmann::json& value)
: text_(value.dump())
{
}
std::optional<std::string>
content_type() const
{
return "application/json";
}
std::optional<std::uint64_t>
content_length() const noexcept
{
return text_.size();
}
capy::io_task<>
write(capy::any_buffer_sink& sink) const
{
auto [ec, n] = co_await sink.write(capy::make_buffer(text_));
co_return { ec };
}
};
return json_body{ value };
}
Because content_length() returns a size, the request goes out with a
Content-Length header; had it returned std::nullopt, the body would be sent
with chunked transfer encoding instead. The application/json from
content_type() is used unless the request already sets that header explicitly.
Receiving: body_to_tag
To read a response into a value with as<T> or
try_as<T>, provide a tag_invoke overload
taking body_to_tag<T> and the response. It returns a
capy::io_task<T> that reads the body and converts it:
capy::io_task<nlohmann::json>
tag_invoke(burl::body_to_tag<nlohmann::json>, burl::response& resp)
{
// Try the parser's in-place buffer first; it is allocation-free
// when the body fits.
auto [ec, sv] = co_await resp.try_as_view();
// Fall back to a heap string when the body is larger than the buffer.
std::string st;
if(ec == boost::http::error::in_place_overflow)
{
auto [sec, body] = co_await resp.try_as<std::string>();
ec = sec;
st = std::move(body);
sv = st;
}
if(ec)
co_return { ec, {} };
// Surface a parse failure as an error rather than a discarded value.
auto doc = nlohmann::json::parse(sv, nullptr, false);
if(doc.is_discarded())
co_return { make_error_code(std::errc::bad_message), {} };
co_return { {}, std::move(doc) };
}
This follows the recommended pattern for reading a body. It first calls
try_as_view, which is allocation-free when the body
fits the parser’s in-place buffer, and only on http::error::in_place_overflow
falls back to reading into a std::string. Building on the response’s own
readers rather than raw I/O inherits its timeout
handling and buffer reuse. A failed parse is surfaced as an error_code rather
than a silently discarded value.
Using It
With both overloads in place, nlohmann::json is just another body type: the
same body and as<T> calls
that drive the built-ins now work on it unchanged:
nlohmann::json body({ { "user", "John" }, { "lang", "En" } });
auto r1 = co_await client.post("https://postman-echo.com/post")
.body(body)
.as<nlohmann::json>();
std::cout << r1.dump(4) << '\n';
When the value is a literal, you can name the type on
body<T> to construct it inline:
auto r2 = co_await client.post("https://postman-echo.com/post")
.body<nlohmann::json>({ 1, 2, 3 })
.as<nlohmann::json>();
std::cout << r2.dump(4) << '\n';
Next Steps
-
Extending — The two
tag_invokeoverloads in depth, including forwarding extra arguments -
Request Bodies — The built-in body types and how framing is chosen
-
Responses — The built-in conversions and
try_as_view