Boost C++ Libraries Home Libraries People FAQ More

PrevUpHomeNext

Value Conversion

While the value container makes it easy to create ad-hoc structures, often it is necessary to convert between JSON and specific user-defined types. Converting from a type T to value is done by value_from. The conversion in the opposite direction is done with value_to.

std::vector< int > v1{ 1, 2, 3, 4 };

// Convert the vector to a JSON array
value jv = value_from( v1 );

assert( jv.is_array() );

array& ja = jv.as_array();

assert( ja.size() == 4 );

for ( std::size_t i = 0; i < v1.size(); ++i )
    assert( v1[i] == ja[i].as_int64() );

// Convert back to vector< int >
std::vector< int > v2 = value_to< std::vector< int > >( jv );

assert( v1 == v2 );
Customization Points

A customization point is a mechanism where a library delegates behavior of some operation to the user, or gives the use the option of controlling the behavior of some operation for a specific type. Within the standard library, the swap function is a customization point that uses argument-dependent lookup to find user-provided overloads within the namespace of the arguments:

template< class T >
void identity_swap( T& a, T& b )
{
    // introduces the declaration of
    // std::swap into this scope
    using std::swap;
    if( &a == &b )
        return;
    // the overload set will contain std::swap,
    // any declarations of swap within the enclosing
    // namespace, and any declarations of swap within
    // the namespaces associated with T
    swap( a, b );
}

Another example would be the class template std::hash, which can be specialized for some type T to implement custom behavior:

template< std::size_t N >
struct static_string { };

namespace std
{

template< std::size_t N >
class hash< static_string< N > >
{
public:
    std::size_t
    operator()(const static_string< N >& str ) const noexcept
    {
        return std::hash< std::string >()( str );
    }
};

} // std

While these mechanisms work, they are not without problems. Boost.JSON implements value conversion customization points using the tag_invoke mechanism outlined in P1895, allowing users to define conversions to and from their own types. In essence, tag_invoke provides a uniform interface for defining customization points by using argument-dependent lookup to find a viable function from the point at which it's called. As the name suggests, a tag type is passed as an argument in order to:

This has the effect of finding user-provided tag_invoke overloads, even if they are declared (lexically) after the definition of the calling function.

tag_invoke overloads

In all cases, conversions are done by calling an appropriate overload of tag_invoke. For value_from, these have the form:

void tag_invoke( const value_from_tag&, value&, T );

Likewise, the overloads of tag_invoke called by value_to take the form:

T tag_invoke( const value_to_tag< T >&, const value& );

In both cases, overloads for user-provided types can be implemented:

template< class T >
void
tag_invoke( const value_from_tag&, value& jv, std::complex< T > const& t)
{
    // Store a complex number as a 2-element array
    // with the real part followed by the imaginary part
    jv = { t.real(), t.imag() };
}

template< class T >
std::complex< T >
tag_invoke( const value_to_tag< std::complex< T > >&, value const& jv )
{
    return std::complex< T >(
        jv.as_array().at(0).to_number< T >(),
        jv.as_array().at(1).to_number< T >());
}

Since the type being converted is embedded into the function's signature, user-provided overloads are visible to argument-dependent lookup and will be candidates when a conversion is performed:

template< class T >
struct vec3
{
    T x, y, z;
};

template< class T >
void tag_invoke( const value_from_tag&, value& jv, const vec3<T>& vec )
{
    jv = {
        { "x", vec.x },
        { "y", vec.y },
        { "z", vec.z }
    };
}

When value_from is called, the tag_invoke function template will be found by argument-dependent lookup and used to perform the conversion:

vec3< int > pos = { 4, 1, 4 };

value jv = value_from( pos );

assert( serialize( jv ) == "{\"x\":4,\"y\":1,\"z\":4}" );

In addition to user-provided overloads of tag_invoke, the library will add its own function to the overload set when certain constraints are satisfied. The library provided overloads have no special prioritization over those provided by the user, so care should be taken to avoid writing ambiguous declarations:

template< class T, typename std::enable_if<
    std::is_floating_point< T >::value>::type* = nullptr >
void tag_invoke( const value_from_tag&, value& jv, T t )
{
    jv = std::llround( t );
}

Upon calling this function, overload resolution will fail because the library already provides an overload for floating-point types:

value jv = value_from( 1.5 ); // error

Library-provided overloads of tag_invoke come in two variants: those that convert between JSON types (known as built-in conversions), and those that convert to/from container and string types (known as generic conversions). Generic conversions offer convenience by eliminating the need to write repetitive overloads for types that model common C++ concepts (e.g. sequence containers, associative containers, tuples, and strings).

std::map< std::string, vec3< int > > positions = {
    { "Alex", { 42, -60, 18 } },
    { "Blake", { 300, -60, -240} },
    { "Carol", { -60, 30, 30 } }
};

// conversions are applied recursively;
// the key type and value type will be converted
// using value_from as well
value jv = value_from( positions );

assert( jv.is_object() );

object& jo = jv.as_object();

assert( jo.size() == 3 );

// The sum of the coordinates is 0
assert( std::accumulate( jo.begin(), jo.end(), std::int64_t(0),
    []( std::int64_t total, const key_value_pair& jp )
    {
        assert ( jp.value().is_object() );

        const object& pos = jp.value().as_object();

        return total + pos.at( "x" ).as_int64() +
            pos.at( "y" ).as_int64() +
            pos.at( "z" ).as_int64();

    } ) == 0 );
Converting to json::value

The function template value_from provides an interface to construct a value from a user- or library-provided type T. The optionally supplied storage_ptr argument is used as the memory_resource for the resulting value object. The parameter of type value& is the result of the conversion; this ensures that the storage_ptr is correctly propagated to the result. For example, consider the following struct:

struct customer
{
    std::uint64_t id;
    std::string name;
    bool late;

    customer() = default;

    customer( std::uint64_t i, const std::string& n, bool l )
        : id( i ), name( n ), late( l ) { }

    explicit customer( value const& );
};

void tag_invoke( const value_from_tag&, value& jv, customer const& c )
{
    // Assign a JSON value
    jv = {
        { "id", c.id },
        { "name", c.name },
        { "late", c.late }
    };
}

If our store has a lot of customers, it may be desirable to use a monotonic_resource when serializing customer objects to JSON. value_from ensures that the correct memory_resource is used:

std::vector< customer > customers = {
    customer( 0, "Alison", false ),
    customer( 1, "Bill", false ),
    customer( 3, "Catherine", true ),
    customer( 4, "Doug", false )
 };

storage_ptr sp = make_shared_resource< monotonic_resource >();

value jv = value_from( customers, sp );

assert( jv.storage() == sp );

assert( jv.is_array() );

In addition to the user-provided overloads found by argument-dependent lookup, the library provides its own overload of tag_invoke when certain conditions are met.

If, for the type T being converted

Then a function template of the form

template< class T >
void tag_invoke( value_from_tag, value& jv, T&& t );

is added to the set of user-provided overloads found by argument-dependent lookup; it performs the conversion corresponding to first condition met by T in the above list. For example, if T satisfies both FromMapLike and FromContainerLike, the conversion will be performed the one corresponding to FromMapLike; it will not be ambiguous.

// Satisfies both FromMapLike and FromContainerLike
std::unordered_map< std::string, bool > available_tools = {
    { "Crowbar", true },
    { "Hammer", true },
    { "Drill", true },
    { "Saw", false },
};

value jv = value_from( available_tools );

assert( jv.is_object() );

The conversion performed when the first condition is met (the library-provided built-in conversion) is assignment to the value parameter. For the generic conversions, types that satisfy TupleLike or FromContainerLike are converted to array, those that satisfy FromMapLike are converted to object, and types that satisfy StringLike are converted to string.

Converting to Foreign Types

The function template value_to provides an interface to construct a type T from a value. In contrast to value_from, no output parameter is used as there is no storage_ptr to propagate.

std::complex< double > c1 = { 3.14159, 2.71828 };

// Convert a complex number to JSON
value jv = value_from( c1 );

assert ( jv.is_array() );

// Convert back to a complex number

std::complex< double > c2 = value_to< std::complex< double > >( jv );

As with value_from, the library provides its own overload of tag_invoke when certain conditions are met.

If, for the type T

Then a function template of the form

template< class T >
T tag_invoke( value_to_tag< T >, const value& jv );

is added to the set of user-provided overloads found by argument-dependent lookup. As with value_from, it performs the conversion corresponding to first condition met by T in the above list. Given the following definition of customer::customer( const value& ):

customer tag_invoke( const value_to_tag<customer>&, const value& jv )
{
    // at() throws if `jv` is not an object, or if the key is not found.
    // as_uint64() will throw if the value is not an unsigned 64-bit integer.
    std::uint64_t id = jv.at( "id" ).as_uint64();

    // We already know that jv is an object from
    // the previous call to jv.as_object() succeeding,
    // now we use jv.get_object() which skips the
    // check. value_to will throw if jv.kind() != kind::string
    std::string name = value_to< std::string >( jv.get_object().at( "name" ) );

    // id and name are constructed from JSON in the member
    // initializer list above, but we can also use regular
    // assignments in the body of the function as shown below.
    // as_bool() will throw if kv.kind() != kind::bool
    bool late = jv.get_object().at( "late" ).as_bool();

    return customer(id, name, late);
}

Objects of type customer can be converted to and from value:

customer c1( 5, "Ed", false );

// Convert customer to value
value jv = value_from( c1 );

// Convert the result back to customer
customer c2 = value_to< customer >( jv );

// The resulting customer is unchanged
assert( c1.name == c2.name );

When the first condition is met, the conversion will simply return the object of type T stored within the value (e.g. using jv.as_object(), jv.as_array(), etc). When the second condition is met, the result of the conversion will be T(jv). As with value_from, when generic conversions are selected, an attempt will be made to convert the value to T.

value available_tools = {
    { "Crowbar", true },
    { "Hammer", true },
    { "Drill", true },
    { "Saw", false }
};

assert( available_tools.is_object() );

auto as_map = value_to< std::map< std::string, bool > >( available_tools );

assert( available_tools.as_object().size() == as_map.size() );
Named Requirements for Generic Conversions

Each of the following tables specify valid operations on a type or expression thereof meeting the requirement R. A requirement Req prefixed with From/To does not define a single requirement; it defines the two requirements FromReq and ToReq which correspond to value_to and value_from, respectively.

In each of the following:

TupleLike Requirements

Table 1.3. Valid expressions

Expression

Type

Semantics and Constraints

std::tuple_size<T>

U

Constraints: U::value is greater than 0


StringLike Requirements

Table 1.4. Valid expressions

Expression

Type

Semantics and Constraints

std::is_constructible<T, const char*, std::size_t>

U

Constraints: U::value is true

e.data()

pointer

Constraints: std::is_convertible<pointer, const char*>::value is true

e.size()

size_type

Constraints: std::is_convertible<size_type, std::size_t>::value is true


From/To ContainerLike Requirements

Table 1.5. Valid expressions

Expression

Type

Semantics and Constraints

T::value_type

value_type

Constraints: has_value_trait<value_type>::value is true

begin(e)

iterator

end(e)

iterator


From/To MapLike Requirements

In the following table vt is a prvalue of type T::value_type.

Table 1.6. Valid expressions

Expression

Type

Semantics and Constraints

begin(e)

iterator

end(e)

iterator

T::value_type

pair_type

Constraints: pair_type satisfies TupleLike and std::tuple_size<pair_type>::value is 2

std::tuple_element<0, pair_type>::type

key_type

Constraints for FromMapLike: std::is_convertible<key_type, string_view>::value is true

Constraints for ToMapLike: std::is_constructible<key_type, string_view>::value is true

std::tuple_element<1, pair_type>::type

value_type

Constraints: has_value_trait<value_type>::value is true

e.emplace(vt)

U

Constraints: U satisfies TupleLike



PrevUpHomeNext