JSON Voorhees: Serialization Builder DSL (original) (raw)
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:
struct person
{
std::string first_name;
std::string last_name;
int age;
std::string role;
};
struct company
{
std::string name;
bool certified;
std::vector employees;
std::list candidates;
};
...and easily convert it to an from a JSON representation that looks like this:
{
"name": "Paul's Construction",
"certified": false,
"employees": [
{
"first_name": "Bob",
"last_name": "Builder",
"age": 29
},
{
"first_name": "James",
"last_name": "Johnson",
"age": 38,
"role": "Foreman"
}
],
"candidates": [
{
"firstname": "Adam",
"lastname": "Ant"
}
]
}
To define a formats for this person type using the serialization builder DSL, you would say:
.type()
.member("first_name", &person::first_name)
.alternate_name("firstname")
.member("last_name", &person::last_name)
.alternate_name("lastname")
.member("age", &person::age)
.until({ 6,1 })
.default_value(21)
.default_on_null()
.check_input([] (int value) { if (value < 0) throw std::logic_error("Age must be positive."); })
.member("role", &person::role)
.since({ 2,0 })
.default_value("Builder")
.type()
.member("name", &company::name)
.member("certified", &company::certified)
.member("employees", &company::employees)
.member("candidates", &company::candidates)
.register_containers<company, std::vector, std::list>()
;
Reference
The DSL is made up of three major parts:
- formats – modifies a
[jsonv::formats](classjsonv%5F1%5F1formats.html "Simply put, this class is a collection of extractor and serializer instances. ")object by adding new type adapters to it - type – modifies the behavior of a
[jsonv::adapter](classjsonv%5F1%5F1adapter.html "An adapter is both an extractor and a serializer. ")by adding new members to it - member – modifies an individual member inside of a specific type
Each 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.
Formats Context
Commands in this section modify the behavior of the underlying [jsonv::formats](classjsonv%5F1%5F1formats.html "Simply put, this class is a collection of extractor and serializer instances. ") object.
Level
check_references
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](classjsonv%5F1%5F1formats.html#ad194791e293229fb6a79438a6444f51b "Get the default formats instance. ")). 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:
There are 2 types referenced that the formats do not know how to serialize:
- date_type (referenced by: name_space::foo, other:📛:space::bar)
- tree
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.
Note
This is evaluated immediately, so it is best to call this function as the very last step in the DSL.
reference_type
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.
.reference_type(std::type_index(typeid(int)), std::type_index(typeid(my_type)))
.reference_type(std::type_index(typeid(my_type))
register_adapter
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_adapter(my_type::get_adapter())
register_optional
register_optional<TOptional>()
Similar to register_adapter, but automatically create an [optional_adapter](classjsonv%5F1%5F1optional%5F%5Fadapter.html "An adapter for optional-like types. ")<TOptional> to store.
.register_optional<std::optional>()
.register_optional<boost::optional>()
register_container
register_container<TContainer>()
Similar to register_adapter, but automatically create a [container_adapter](classjsonv%5F1%5F1container%5F%5Fadapter.html "An adapter for container types. ")<TContainer> to store.
.register_container<std::vector>()
.register_container<std::liststd::string>()
register_containers
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_containers<int, std::list, std::deque>()
.register_containers<double, std::vector, std::set>()
Note
Not supported in MSVC 14 (CTP 5).
register_wrapper
register_wrapper<TWrapper>()
Similar to register_adapter, but automatically create an [wrapper_adapter](classjsonv%5F1%5F1wrapper%5F%5Fadapter.html "An adapter for "wrapper" types. ")<TWrapper> to store.
.register_optional<std::optional>()
.register_optional<boost::optional>()
enum_type
enum_type<TEnum>(std::string name, std::initializer_list<std::pair<TEnum, [jsonv::value](classjsonv%5F1%5F1value.html "Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...")>>)enum_type_icase<TEnum>(std::string name, std::initializer_list<std::pair<TEnum, [jsonv::value](classjsonv%5F1%5F1value.html "Represents a single JSON value, which can be any one of a potential kind, each behaving slightly diff...")>>)
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.
.enum_type("ring",
{
{ ring::fire, "fire" },
{ ring::wind, "wind" },
{ ring::earth, "earth" },
{ ring::water, "water" },
{ ring::heart, "heart" },
{ ring::heart, "useless" },
{ ring::fire, 1 },
{ ring::ussr, "wind" },
}
)
.enum_type_icase("integer",
{
{ 0, "zero" },
{ 0, "naught" },
{ 1, "one" },
{ 2, "two" },
{ 3, "three" },
}
)
See also
polymorphic_type
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.
.polymorphic_type<std::unique_ptr>("type")
.subtype<derived_1>("derived_1")
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
extend(std::function<void ([formats_builder](classjsonv%5F1%5F1formats%5F%5Fbuilder.html)&)> func)
Extend the [formats_builder](classjsonv%5F1%5F1formats%5F%5Fbuilder.html) with the provided func by passing the current builder to it. This provides a more convenient way to call helper functions.
foo(builder);
bar(builder);
baz(builder);
This can be done equivalently with:
.extend(foo)
.extend(bar)
.extend(baz)
on_duplicate_type
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](classjsonv%5F1%5F1formats%5F%5Fbuilder.html) 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](classjsonv%5F1%5F1formats%5F%5Fbuilder.html).
Narrowing
type
type<T>()type<T>(std::function<void ([adapter_builder](classjsonv%5F1%5F1adapter%5F%5Fbuilder.html)<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 this call to type creates, which can be used for creating common extension functions.
.type<my_type>()
.member(...)
.
.
.
Type Context
Commands in this section modify the behavior of the [jsonv::adapter](classjsonv%5F1%5F1adapter.html "An adapter is both an extractor and a serializer. ") for a particular type.
Level
pre_extract
pre_extract(std::function<void (const [extraction_context](classjsonv%5F1%5F1extraction%5F%5Fcontext.html)& 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
post_extract(std::function<T (const [extraction_context](classjsonv%5F1%5F1extraction%5F%5Fcontext.html)& 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()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.
serialization_builder_dsl_ref_type_level_type_default_value
type_default_value(T value)type_default_value(std::function<T (const [extraction_context](classjsonv%5F1%5F1extraction%5F%5Fcontext.html)& context)>)
What value should be used to create the default for this type?
.type<my_type>()
.type_default_on_null()
.type_default_value(my_type("default"))
on_extract_extra_keys
on_extract_extra_keys(std::function<void (const [extraction_context](classjsonv%5F1%5F1extraction%5F%5Fcontext.html)& 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).
.type<my_type>()
.member("x", &my_type::x)
.member("y", &my_type::y)
.on_extract_extra_keys([] (const extraction_context&, const value&, std::setstd::string extra_keys)
{
throw extracted_extra_keys("my_type", std::move(extra_keys));
}
)
There is a convenience function named throw_extra_keys_extraction_error which does this for you.
.type<my_type>()
.member("x", &my_type::x)
.member("y", &my_type::y)
.on_extract_extra_keys(jsonv::throw_extra_keys_extraction_error)
Narrowing
member
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.
.type<my_type>()
.member("x", &my_type::x)
.member("y", &my_type::y)
.member("thing", &my_type::get_thing, &my_type::set_thing)
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.
Level
after
after(version)
Only serialize this member if the [serialization_context::version](classjsonv%5F1%5F1context%5F%5Fbase.html#ad561e860ce9209021de176f646538f3f "Get the version this extraction_context was created with. ") is not [version::empty](structjsonv%5F1%5F1version.html#a0571c645cd90fdc6fbdfb55a5a1438b5 "Check if this version is an "empty" value – meaning major and minor are both 0. ") and is greater than or equal to the provided version.
alternate_name
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
before(version)
Only serialize this member if the [serialization_context::version](classjsonv%5F1%5F1context%5F%5Fbase.html#ad561e860ce9209021de176f646538f3f "Get the version this extraction_context was created with. ") is not [version::empty](structjsonv%5F1%5F1version.html#a0571c645cd90fdc6fbdfb55a5a1438b5 "Check if this version is an "empty" value – meaning major and minor are both 0. ") and is less than or equal to the provided version.
check_input
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).
.member("x", &my_type::x)
.check_input([] (int x) { if (x < 0) throw std::logic_error("x must be greater than 0"); })
.check_input([] (int x) { return x < 100; }, [] (int x) { throw exceptions::less_than(100, x); })
.check_input([] (int x) { return x % 2 == 0; }, std::logic_error("x must be divisible by 2"))
default_value
default_value(TMember value)default_value(std::function<TMember (const [extraction_context](classjsonv%5F1%5F1extraction%5F%5Fcontext.html)&, 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.
.member("x", &my_type::x)
.default_value(10)
default_on_null
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
encode_if(std::function<bool (const [serialization_context](classjsonv%5F1%5F1serialization%5F%5Fcontext.html)&, const TMember&)> check)
Only serialize this member if the check function returns true.
since
since(version)
Only serialize this member if the [serialization_context::version](classjsonv%5F1%5F1context%5F%5Fbase.html#ad561e860ce9209021de176f646538f3f "Get the version this extraction_context was created with. ") is not [version::empty](structjsonv%5F1%5F1version.html#a0571c645cd90fdc6fbdfb55a5a1438b5 "Check if this version is an "empty" value – meaning major and minor are both 0. ") and is greater than the provided version.
until
until(version)
Only serialize this member if the [serialization_context::version](classjsonv%5F1%5F1context%5F%5Fbase.html#ad561e860ce9209021de176f646538f3f "Get the version this extraction_context was created with. ") is not [version::empty](structjsonv%5F1%5F1version.html#a0571c645cd90fdc6fbdfb55a5a1438b5 "Check if this version is an "empty" value – meaning major and minor are both 0. ") and is less than the provided version.