JSON Voorhees
Killer JSON for C++
|
Most applications tend to have a lot of structure types.
While it is possible to write an extractor
and serializer
(or adapter
) for each type, this can get a little bit tedious. Beyond that, it is very difficult to look at the contents of adapter code and discover what the JSON might actually look like. The builder DSL is meant to solve these issues by providing a convenient way to describe conversion operations for your C++ types.
At the end of the day, the goal is to take some C++ structures like this:
...and easily convert it to an from a JSON representation that looks like this:
To define a formats
for this person
type using the serialization builder DSL, you would say:
The DSL is made up of three major parts:
jsonv::formats
object by adding new type adapters to itjsonv::adapter
by adding new members to itEach successive function call transforms your context. Narrowing calls make your context more specific; for example, calling type
from a formats context allows you to modify a specific type. Widening calls make the context less specific and are always available; for example, when in the member context, you can still call type
from the formats context to specify a new type.
Commands in this section modify the behavior of the underlying jsonv::formats
object.
check_references(formats)
check_references(formats, std::string name)
check_references(formats::list)
check_references(formats::list, std::string name)
check_references()
check_references(std::string name)
Tests that every type referenced by the members of the output of the DSL have an extractor
and a serializer
. The provided formats
is used to draw extra types from (a common value is jsonv::formats::defaults
). In other words, it asks the question: If the formats
from this DSL was combined with these other formats
, could all of the types be encoded and decoded?
This does not mutate the DSL in any way. On successful verification, it will appear that nothing happened. If the verification is not successful, an exception will be thrown with the offending types in the message. For example:
If name is provided, the value will be output to the error message on failure. This can be useful if you have multiple check_references
statements and wish to more easily determine the failing formats
combination from the error message alone.
reference_type(std::type_index type)
reference_type(std::type_index type, std::type_index from)
Explicitly add a reference to the provided type in the DSL. If from is provided, also add a back reference for tracking purposes. The from field is useful for tracking why the type is referenced.
Type references are used in check_references to both check and generate error messages if the formats
the DSL is building cannot fully create and extract JSON values. You do not usually have to call this, as each call to member calls this automatically.
register_adapter(const adapter*)
register_adapter(std::shared_ptr<const adapter>)
Register an arbitrary adapter
with the formats
we are currently building. This is useful for integrating with type adapters that do not (or can not) use the DSL.
register_optional<TOptional>()
Similar to register_adapter
, but automatically create an optional_adapter<TOptional>
to store.
register_container<TContainer>()
Similar to register_adapter
, but automatically create a container_adapter<TContainer>
to store.
register_containers<T, template <T, ...>... TTContainer>
Convenience function for calling register_container
for multiple containers with the same value_type
. Unfortunately, it only supports varying the first template parameter of the TTContainer
types, so if you wish to do something like vary the allocator, you will have to either call register_container
multiple times or use a template alias.
register_wrapper<TWrapper>()
Similar to register_adapter
, but automatically create an wrapper_adapter<TWrapper>
to store.
enum_type<TEnum>(std::string name, std::initializer_list<std::pair<TEnum, jsonv::value>>)
enum_type_icase<TEnum>(std::string name, std::initializer_list<std::pair<TEnum, jsonv::value>>)
Create an adapter for the TEnum
type with a mapping of C++ values to JSON values and vice versa. The most common use of this is to map enum
values in C++ to string representations in JSON. TEnum
is not restricted to types which are enum
, but can be anything which you would like to restrict to a limited subset of possible values. Likewise, JSON representations are not restricted to being of kind::string
.
The sibling function enum_type_icase
will create an adapter which uses case-insensitive checking when converting to C++ values in extract
.
polymorphic_type<<TPointer>(std::string discrimination_key);
Create an adapter for the TPointer
type (usually std::shared_ptr
or std::unique_ptr
) that knows how to serialize and deserialize one or more types that can be polymorphically represented by TPointer
, i.e. derived types. It uses a discrimination key to determine which concrete type should be instantiated when extracting values from json.
The keyed_subtype_action can be used to configure the adapter to make sure that the discrimination key was correctly serialized (keyed_subtype_action::check) or to insert the discrimination key for the underlying type so that the underlying type doesn't need to do that itself (keyed_subtype_action::insert). The default is to do nothing (keyed_subtype_action::none).
extend(std::function<void (formats_builder&)> func)
Extend the formats_builder
with the provided func by passing the current builder to it. This provides a more convenient way to call helper functions.
This can be done equivalently with:
on_duplicate_type(on_duplicate_type_action action);
Set what action to take when attempting to register an adapter, but there is already an adapter for that type in the formats. The default is to throw a duplicate_type_error exception (duplicate_type_action::exception), but the formats_builder
can also be configured to ignore the duplicate (duplicate_type_action::ignore), or to replace the existing adapter with the new one (duplicate_type_action::replace). This is useful when calling multiple extend
methods that may add common types to the formats_builder
.
type<T>()
type<T>(std::function<void (adapter_builder<T>&)> func)
Create an adapter
for type T
and begin building the members for it. If func is provided, it will be called with the adapter_builder<T> this call to type
creates, which can be used for creating common extension functions.
Commands in this section modify the behavior of the jsonv::adapter
for a particular type.
pre_extract(std::function<void (const extraction_context& context, const value& from)> perform)
Call the given perform function during the extract
operation, but before performing any extraction. This can be called multiple times – all functions will be called in the order they are provided.
post_extract(std::function<T (const extraction_context& context, T&& out)> perform)
Call the given perform function after the extract
operation. All functions will be called in the order they are provided. This allows validation methods to be called on the extracted object as part of extraction. Postprocessing functions are allowed to mutate the extracted object.
type_default_on_null()
type_default_on_null(bool on)
If the JSON value null
is in the input, should this type take on some default? This should be used with serialization_builder_dsl_ref_type_level_type_default_value type_default_value.
type_default_value(T value)
type_default_value(std::function<T (const extraction_context& context)>)
What value should be used to create the default for this type?
on_extract_extra_keys(std::function<void (const extraction_context& context, const value& from, std::set<std::string> extra_keys)> action )
When extracting, perform some action if extra keys are provided. By default, extra keys are usually simply ignored, so this is useful if you wish to throw an exception (or anything you want).
There is a convenience function named throw_extra_keys_extraction_error
which does this for you.
member(std::string name, TMember T::*selector)
member(std::string name, const TMember& (*access)(const T&), void (*mutate)(T&, TMember&&))
member(std::string name, const TMember& (T::*access)() const, TMember& (T::*mutable_access)())
member(std::string name, const TMember& (T::*access)() const, void (T::*mutate)(TMember))
member(std::string name, const TMember& (T::*access)() const, void (T::*mutate)(TMember&&))
Adds a member to the type we are currently building. By default, the member will be serialized with the key of the given name and the extractor will search for the given name. If you wish to change properties of this field, use the Member Context.
Commands in this section modify the behavior of a particular member. Here, T
refers to the containing type (the one we are adding a member to) and TMember
refers to the type of the member we are modifying.
after(version)
Only serialize this member if the serialization_context::version
is not version::empty
and is greater than or equal to the provided version
.
alternate_name(std::string name)
Provide an alternate name to search for when extracting this member. If a user provides values for multiple names, preference is given to names earlier in the list, starting with the original given name.
before(version)
Only serialize this member if the serialization_context::version
is not version::empty
and is less than or equal to the provided version
.
check_input(std::function<void (const TMember&)> check)
check_input(std::function<bool (const TMember&)> check, std::function<void (const TMember&)> thrower)
check_input(std::function<bool (const TMember&)> check, TException ex)
Checks the extracted value with the given check function. In the first form, you are expected to throw inside the function. In the latter forms, the second parameter will be invoked (in the case of thrower) or thrown directly (in the case of ex).
default_value(TMember value)
default_value(std::function<TMember (const extraction_context&, const value&)> create)
Provide a default value for this member if no key is found when extracting. You can use the function implementation to synthesize the key however you want.
default_on_null()
default_on_null(bool on)
If the value associated with this key is kind::null
, should that be treated as the default value? This option is only considered if a default_value default_value was provided.
encode_if(std::function<bool (const serialization_context&, const TMember&)> check)
Only serialize this member if the check function returns true.
since(version)
Only serialize this member if the serialization_context::version
is not version::empty
and is greater than the provided version
.
until(version)
Only serialize this member if the serialization_context::version
is not version::empty
and is less than the provided version
.