Extending

Teaching Burl to send a type as a request body, or to read a response body into a type, is a matter of writing a tag_invoke overload found by argument-dependent lookup. The built-in conversions for strings, JSON, and files are written exactly this way.

Sending a Type as a Request Body

To make a type usable with the builder’s body function, provide a tag_invoke overload taking body_from_tag<T>. It returns an any_request_body, a type-erased wrapper around an object satisfying the RequestBody concept:

struct RequestBody
{
    std::optional<std::string>   content_type()   const;
    std::optional<std::uint64_t> content_length() const;
    capy::io_task<>              write(capy::any_buffer_sink& sink) const;
};

Here is a complete body that serializes an nlohmann::json document. Because the serialized text is materialized first, its size is known, so the body reports a Content-Length:

namespace nlohmann
{
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 };
}
} // namespace nlohmann

The overload is found by argument-dependent lookup, so placing it in the type’s own namespace is enough. The type then works with body like any built-in:

nlohmann::json doc({ { "user", "John" } });

auto r = co_await client.post("https://example.com/post")
    .body(doc)
    .send();

Reading a Response Body into a Type

To make a type usable with xref:reference:boost/burl/response/as.adoc[response::as] and request_builder::as, provide a tag_invoke overload taking body_to_tag<T>. It takes the response and returns a capy::io_task<T> that reads and converts the body:

namespace nlohmann
{
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 == 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) };
}
} // namespace nlohmann

This conversion shows the recommended pattern: read with xref:reference:boost/burl/response/try_as_view.adoc[try_as_view] first, which is allocation-free when the body fits the parser’s in-place buffer, and on http::error::in_place_overflow fall back to reading into a std::string. Build the result on a value-side tag_invoke rather than reaching for raw I/O, and you inherit the timeout handling and the buffer reuse the response already provides.

With both overloads in place, the type is a first-class body in both directions:

auto doc = co_await client.get("https://example.com/data")
    .as<nlohmann::json>();

Forwarding Extra Arguments

Both body and as forward any trailing arguments to the matching tag_invoke, positioned after its fixed parameters. An overload can therefore take configuration that burl knows nothing about.

The built-in file conversion uses this for its destination path. Its overload declares the extra parameter after the response:

capy::io_task<std::filesystem::path>
tag_invoke(
    burl::body_to_tag<std::filesystem::path>,
    burl::response& resp,
    std::filesystem::path dest);

so the trailing argument at the call site lands in dest:

co_await client.get("https://example.com/file")
    .as<std::filesystem::path>("./out.bin");

Next Steps