nlohmann/json

A complete, runnable example of teaching Burl a body type it has never heard of, extracted from example/nlohmann_json.cpp. It puts Extending to work on nlohmann::json: a pair of tag_invoke overloads, found by argument-dependent lookup, that make the type a first-class body in both directions.

Sending: body_from_tag

The body_from_tag<T> overload serializes the document up front with dump(), so the body owns the text and reports a known Content-Length.

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 };
}

See Extending.

Receiving: body_to_tag

The body_to_tag<T> overload reads the body and parses it, following the recommended try_as_view pattern: in place when the body fits the parser’s buffer, falling back to a std::string on overflow.

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) };
}

See Extending.

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_invoke overloads 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