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_point
s
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_point
s 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_address
es and time_point
s 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;
}
}