Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

This is the documentation for a snapshot of the master branch, built from commit 0dfaafd333.
PrevUpHomeNext

Contextual conversions

Previously in this section we've been assuming that there is a particular fitting JSON representation for a type. But this is not always the case. Often one needs to represent particular value with JSON of certain format in one situation and with another format in a different situation. This can be achieved with Boost.JSON by providing an extra argument—context.

Let's implement conversion from user_ns::ip_address to a JSON string:

namespace user_ns
{

struct as_string
{ };

void
tag_invoke(
    boost::json::value_from_tag, boost::json::value& jv, const ip_address& addr, const as_string& )
{
    boost::json::string& js = jv.emplace_string();
    js.resize( 4 * 3 + 3 + 1 ); // XXX.XXX.XXX.XXX\0
    auto it = addr.begin();
    auto n = std::sprintf(
        js.data(), "%hhu.%hhu.%hhu.%hhu", it[0], it[1], it[2], it[3] );
    js.resize(n);
}

ip_address
tag_invoke(
    boost::json::value_to_tag<ip_address>, const boost::json::value& jv, const as_string& )
{
    const boost::json::string& js = jv.as_string();

    unsigned char octets[4];
    int result = std::sscanf(
        js.data(), "%hhu.%hhu.%hhu.%hhu", octets, octets + 1, octets + 2, octets + 3 );
    if( result != 4 )
        throw std::invalid_argument("not an IP address");

    return ip_address( octets[0], octets[1], octets[2], octets[3] );
}

}

These tag_invoke overloads take an extra as_string parameter, which disambiguates this specific representation of ip_address from all other potential representations. In order to take advantage of them one needs to pass an as_string object to value_from or value_to as the last argument:

ip_address addr( 192, 168, 10, 11 );

value jv = value_from( addr, as_string() );
assert( jv == parse(R"( "192.168.10.11" )") );

ip_address addr2 = value_to< ip_address >( jv, as_string() );
assert(std::equal(
    addr.begin(), addr.end(), addr2.begin() ));

Note, that if there is no dedicated tag_invoke overload for a given type and a given context, the implementation falls back to overloads without context. Thus it is easy to combine contextual conversions with conversions provided by the library:

std::map< std::string, ip_address > computers = {
    { "Alex", { 192, 168, 1, 1 } },
    { "Blake", { 192, 168, 1, 2 } },
    { "Carol", { 192, 168, 1, 3 } },
};
value jv = value_from( computers, as_string() );
assert( jv == parse(
    "{                               "
    "    \"Alex\" : \"192.168.1.1\", "
    "    \"Blake\": \"192.168.1.2\", "
    "    \"Carol\": \"192.168.1.3\"  "
    "}                               "
    ) );
Conversions for third-party types

Normally, you wouldn't be able to provide conversions for types from third-party libraries and standard types, because it would require yout to put tag_invoke overloads into namespaces you do not control. But with contexts you can put the overloads into your namespaces. This is because the context will add its associated namespaces into the list of namespaces where tag_invoke overloads are searched.

As an example, let's implement conversion for std::chrono::system_clock::time_points using ISO 8601 format.

namespace user_ns
{

struct as_iso_8601
{ };

void
tag_invoke(
    boost::json::value_from_tag, boost::json::value& jv, std::chrono::system_clock::time_point tp, const as_iso_8601& )
{
    boost::json::string& js = jv.emplace_string();
    js.resize( 4 + 2 * 5 + 5 + 1 ); // YYYY-mm-ddTHH:MM:ss\0

    std::time_t t = std::chrono::system_clock::to_time_t( tp );
    std::tm tm = *std::gmtime( &t );
    std::size_t n = std::strftime(
        js.data(), js.size(), "%FT%T", &tm );
    js.resize(n);
}

}

Reverse conversion is left out for brevity.

The new context is used in a similar fashion to as_string previously in this section.

std::chrono::system_clock::time_point tp;
value jv = value_from( tp, as_iso_8601() );
assert( jv == parse(R"( "1970-01-01T00:00:00" )") );

One particular use case that is enabled by contexts is adaptor libraries that define JSON conversion logic for types from a different library.

Passing data to conversion functions

Contexts we used so far were empty classes. But contexts may have data members and member functions just as any class. Implementers of conversion functions can take advantage of that to have conversions configurable at runtime or pass special objects to conversions (e.g. allocators).

Let's rewrite conversion for system_clock::time_points to allow any format supported by std::strftime.

namespace user_ns
{

struct date_format
{
    std::string format;
    std::size_t buffer_size;
};

void
tag_invoke(
    boost::json::value_from_tag, boost::json::value& jv, std::chrono::system_clock::time_point tp, const date_format& ctx )
{
    boost::json::string& js = jv.emplace_string();
    js.resize( ctx.buffer_size );

    std::time_t t = std::chrono::system_clock::to_time_t( tp );
    std::size_t n = std::strftime(
        js.data(), js.size(), ctx.format.c_str(), std::gmtime( &t ) );
    js.resize(n);
}

}

This tag_invoke overload lets us change date conversion format at runtime. Also note, that there is no ambiguity between as_iso_8601 overload and date_format overload. You can use both in your program:

std::chrono::system_clock::time_point tp;

value jv = value_from( tp, date_format{ "%T %D", 18 } );
assert( jv == parse(R"( "00:00:00 01/01/70" )") );

jv = value_from( tp, as_iso_8601() );
assert( jv == parse(R"( "1970-01-01T00:00:00" )") );
Combining contexts

Often it is needed to use several conversion contexts together. For example, consider a log of remote users identified by IP addresses accessing a system. We can represent it as std::vector< std::pair<std::chrono::system_clock::time_point, ip_address > >. We want to serialize both ip_addresses and time_points as strings, but for this we need both as_string and as_iso_8601 contexts. To combine several contexts just use std::tuple. Conversion functions will select the first element of the tuple for which a tag_invoke overload exists and will call that overload. As usual, tag_invoke overloads that don't use contexts and library-provided generic conversions are also supported. Thus, here's our example:

using time_point = std::chrono::system_clock::time_point;
time_point start;
std::vector< std::pair<time_point, ip_address> > log = {
    { start += std::chrono::seconds(10), {192, 168, 10, 11} },
    { start += std::chrono::hours(2),    {192, 168, 10, 13} },
    { start += std::chrono::minutes(14), {192, 168, 10, 10} },
};
value jv = value_from(
    log, std::make_tuple( as_string(), as_iso_8601() ) );
assert( jv == parse(
    " [                                                   "
    "     [ \"1970-01-01T00:00:10\", \"192.168.10.11\" ], "
    "     [ \"1970-01-01T02:00:10\", \"192.168.10.13\" ], "
    "     [ \"1970-01-01T02:14:10\", \"192.168.10.10\" ]  "
    " ]                                                   "
    ) );

In this snippet time_point is converted using tag_invoke overload that takes as_iso_8601, ip_address is converted using tag_invoke overload that takes as_string, and std::vector is converted using a generic conversion provided by the library.

Contexts and composite types

As was shown previously, generic conversions provided by the library forward contexts to conversions of nested objects. And in the case when you want to provide your own conversion function for a composite type enabled by a particular context, you usually also need to do that.

Consider this example. As was discussed in a previous section, is_map_like requires that your key type satisfies is_string_like. Now, let's say your keys are not string-like, but they do convert to string. You can make such maps to also convert to objects using a context. But if you want to also use another context for values, you need a way to pass the full combined context to map elements. So, you want the following test to succeed.

std::map< time_point, ip_address > log = {
    { start += std::chrono::seconds(10), {192, 168, 10, 11} },
    { start += std::chrono::hours(2),    {192, 168, 10, 13} },
    { start += std::chrono::minutes(14), {192, 168, 10, 10} },
};

value jv = value_from(
    log,
    std::make_tuple( maps_as_objects(), as_string(), as_iso_8601() ) );
assert( jv == parse(
    " {                                               "
    "     \"1970-01-01T00:00:10\": \"192.168.10.11\", "
    "     \"1970-01-01T02:00:10\": \"192.168.10.13\", "
    "     \"1970-01-01T02:14:10\": \"192.168.10.10\"  "
    " }                                               "
    ) );

For this you will have to use a different overload of tag_invoke. This time it has to be a function template, and it should have two parameters for contexts. The first parameter is the specific context that disambiguates that particular overload. The second parameter is the full context passed to value_to or value_from.

namespace user_ns
{

struct maps_as_objects
{ };

template<
    class Key,
    class Value,
    class Ctx >
void
tag_invoke(
    boost::json::value_from_tag,
    boost::json::value& jv,
    const std::map<Key, Value>& m,
    const maps_as_objects&,
    const Ctx& ctx )
{
    boost::json::object& jo = jv.emplace_object();

    for( const auto& item: m )
    {
        auto k = boost::json::value_from( item.first, ctx, jo.storage() );
        auto v = boost::json::value_from( item.second, ctx, jo.storage() );
        jo[std::move( k.as_string() )] = std::move( v );
    }
}

template<
    class Key,
    class Value,
    class Ctx >
std::map<Key, Value>
tag_invoke(
    boost::json::value_to_tag< std::map<Key, Value> >,
    boost::json::value const& jv,
    const maps_as_objects&,
    const Ctx& ctx )
{
    const boost::json::object& jo = jv.as_object();
    std::map< Key, Value > result;
    for( auto&& item: jo )
    {
        Key k = boost::json::value_to< Key >( item.key(), ctx );
        Value v = boost::json::value_to< Value >( item.value(), ctx );
        result.emplace( std::move(k), std::move(v) );
    }
    return result;
}

}

PrevUpHomeNext