diff --git a/include/jsonv/serialization_builder.hpp b/include/jsonv/serialization_builder.hpp
index 3fc9d53..516d36c 100644
--- a/include/jsonv/serialization_builder.hpp
+++ b/include/jsonv/serialization_builder.hpp
@@ -293,6 +293,27 @@ namespace jsonv
*
* \see enum_adapter
*
+ * \paragraph serialization_builder_dls_ref_formats_level_polymorphic_type polymorphic_type
+ *
+ * - polymorphic_type<<TPointer>(std::string discrimination_key);
+ *
+ * Create an adapter for the \c TPointer type (usually \c std::shared_ptr or \c std::unique_ptr) that knows how to
+ * serialize and deserialize one or more types that can be polymorphically represented by \c TPointer, i.e. derived
+ * types. It uses a discrimination key to determine which concrete type should be instantiated when extracting values
+ * from json.
+ *
+ * \code
+ * .polymorphic_type>("type")
+ * .subtype("derived_1")
+ * .subtype("derived_2", keyed_subtype_action::check)
+ * .subtype("derived_3", keyed_subtype_action::insert);
+ * \endcode
+ *
+ * The \ref keyed_subtype_action can be used to configure the adapter to make sure that the discrimination key was
+ * correctly serialized (\ref 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 (\ref keyed_subtype_action::insert). The default is to do
+ * nothing (\ref keyed_subtype_action::none).
+ *
* \paragraph serialization_builder_dsl_ref_formats_level_extend extend
*
* - extend(std::function<void (formats_builder&)> func)
@@ -1128,18 +1149,23 @@ class polymorphic_adapter_builder :
}
template
- polymorphic_adapter_builder& subtype(value discrimination_value)
+ polymorphic_adapter_builder& subtype(value discrimination_value,
+ keyed_subtype_action action = keyed_subtype_action::none)
{
if (_discrimination_key.empty())
throw std::logic_error("Cannot use single-argument subtype if no discrimination_key has been set");
- return subtype(_discrimination_key, std::move(discrimination_value));
+ return subtype(_discrimination_key, std::move(discrimination_value), action);
}
template
- polymorphic_adapter_builder& subtype(std::string discrimination_key, value discrimination_value)
+ polymorphic_adapter_builder& subtype(std::string discrimination_key,
+ value discrimination_value,
+ keyed_subtype_action action = keyed_subtype_action::none)
{
- _adapter->template add_subtype_keyed(std::move(discrimination_key), std::move(discrimination_value));
+ _adapter->template add_subtype_keyed(std::move(discrimination_key),
+ std::move(discrimination_value),
+ action);
reference_type(std::type_index(typeid(TSub)), std::type_index(typeid(TPointer)));
return *this;
}
diff --git a/include/jsonv/serialization_util.hpp b/include/jsonv/serialization_util.hpp
index 0b5180a..f8adaf2 100644
--- a/include/jsonv/serialization_util.hpp
+++ b/include/jsonv/serialization_util.hpp
@@ -14,6 +14,7 @@
#define __JSONV_SERIALIZATION_UTIL_HPP_INCLUDED__
#include
+#include
#include
#include
@@ -517,6 +518,22 @@ class enum_adapter :
template >
using enum_adapter_icase = enum_adapter;
+/**
+ * What to do when serializing a keyed subtype of a \ref polymorphic_adapter. See \ref
+ * polymorphic_adapter::add_subtype_keyed.
+**/
+enum class keyed_subtype_action : unsigned char
+{
+ /** Don't do any checking or insertion of the expected key/value pair. **/
+ none,
+ /** Ensure the correct key/value pair was inserted by serialization. Throws \c std::runtime_error if it wasn't. **/
+ check,
+ /** Insert the correct key/value pair as part of serialization. Throws \c std::runtime_error if the key is already
+ * present.
+ **/
+ insert
+};
+
/** An adapter which can create polymorphic types. This allows you to parse JSON directly into a type heirarchy without
* some middle layer.
*
@@ -570,8 +587,14 @@ class polymorphic_adapter :
* \see add_subtype
**/
template
- void add_subtype_keyed(std::string key, value expected_value)
+ void add_subtype_keyed(std::string key,
+ value expected_value,
+ keyed_subtype_action action = keyed_subtype_action::none)
{
+ std::type_index tidx = std::type_index(typeid(T));
+ if (!_serialization_actions.emplace(tidx, std::make_tuple(key, expected_value, action)).second)
+ throw duplicate_type_error("polymorphic_adapter subtype", std::type_index(typeid(T)));
+
match_predicate op = [key, expected_value] (const extraction_context&, const value& value)
{
if (!value.is_object())
@@ -634,15 +657,57 @@ class polymorphic_adapter :
{
if (_check_null_output && !from)
return null;
- else
- return context.to_json(typeid(*from), static_cast(&*from));
+
+ value serialized = context.to_json(typeid(*from), static_cast(&*from));
+
+ auto action_iter = _serialization_actions.find(std::type_index(typeid(*from)));
+ if (action_iter != _serialization_actions.end())
+ {
+ auto errmsg = [&]()
+ {
+ return " polymorphic_adapter<" + demangle(typeid(TPointer).name()) + ">"
+ "subtype(" + demangle(typeid(*from).name()) + ")";
+ };
+
+ const std::string& key = std::get<0>(action_iter->second);
+ const value& val = std::get<1>(action_iter->second);
+ const keyed_subtype_action& action = std::get<2>(action_iter->second);
+
+ switch (action)
+ {
+ case keyed_subtype_action::none:
+ break;
+ case keyed_subtype_action::check:
+ if (!serialized.is_object())
+ throw std::runtime_error("Expected keyed subtype to serialize as an object." + errmsg());
+ if (!serialized.count(key))
+ throw std::runtime_error("Expected subtype key not found." + errmsg());
+ if (serialized.at(key) != val)
+ throw std::runtime_error("Expected subtype key is not the expected value." + errmsg());
+ break;
+ case keyed_subtype_action::insert:
+ if (!serialized.is_object())
+ throw std::runtime_error("Expected keyed subtype to serialize as an object." + errmsg());
+ if (serialized.count(key))
+ throw std::runtime_error("Subtype key already present when trying to insert." + errmsg());
+ serialized[key] = val;
+ break;
+ default:
+ throw std::runtime_error("Unknown keyed_subtype_action.");
+ }
+ }
+
+ return serialized;
}
private:
using create_function = std::function;
private:
+ using serialization_action = std::tuple;
+
std::vector> _subtype_ctors;
+ std::map _serialization_actions;
bool _check_null_input = false;
bool _check_null_output = false;
};
diff --git a/src/jsonv-tests/serialization_builder_tests.cpp b/src/jsonv-tests/serialization_builder_tests.cpp
index 8ee9024..c37e22e 100644
--- a/src/jsonv-tests/serialization_builder_tests.cpp
+++ b/src/jsonv-tests/serialization_builder_tests.cpp
@@ -604,36 +604,93 @@ struct b_derived :
{
builder.member("type", &b_derived::x);
}
-
+
+ static void json_adapt_bad_value(adapter_builder& builder)
+ {
+ builder.member("type", &b_derived::bad);
+ }
+
+ static void json_adapt_no_value(adapter_builder&)
+ {
+ }
+
std::string x = "b";
+ std::string bad = "bad";
+};
+
+struct c_derived :
+ base
+{
+ virtual std::string get() const override { return "c"; }
+
+ static void json_adapt(adapter_builder&)
+ {
+ }
+
+ static void json_adapt_some_value(adapter_builder& builder)
+ {
+ builder.member("type", &c_derived::x);
+ }
+
+ std::string x = "c";
};
}
TEST(serialization_builder_polymorphic_direct)
{
- formats fmts =
- formats::compose
- ({
- formats_builder()
- .polymorphic_type>("type")
- .subtype("a")
- .subtype("b")
- .type(a_derived::json_adapt)
- .type(b_derived::json_adapt)
- .register_container>>()
- .check_references(formats::defaults()),
- formats::defaults()
- });
+ auto make_fmts = [](std::function&)> b_adapter,
+ std::function&)> c_adapter)
+ {
+ return formats::compose
+ ({
+ formats_builder()
+ .polymorphic_type>("type")
+ .subtype("a")
+ .subtype("b", keyed_subtype_action::check)
+ .subtype("c", keyed_subtype_action::insert)
+ .type(a_derived::json_adapt)
+ .type(b_adapter)
+ .type(c_adapter)
+ .register_container>>()
+ .check_references(formats::defaults()),
+ formats::defaults()
+ });
+ };
+
+ auto make_bad_fmts = []()
+ {
+ formats_builder()
+ .polymorphic_type>("type")
+ .subtype("a")
+ .subtype("a");
+ };
- value input = array({ object({{ "type", "a" }}), object({{ "type", "b" }}) });
+ auto fmts = make_fmts(b_derived::json_adapt, c_derived::json_adapt);
+ value input = array({ object({{ "type", "a" }}), object({{ "type", "b" }}), object({{ "type", "c" }}) });
auto output = extract>>(input, fmts);
ensure(output.at(0)->get() == "a");
ensure(output.at(1)->get() == "b");
+ ensure(output.at(2)->get() == "c");
value encoded = to_json(output, fmts);
ensure_eq(input, encoded);
+
+ // If the b type serializes the wrong value for "type" we should get a runtime_error.
+ fmts = make_fmts(b_derived::json_adapt_bad_value, c_derived::json_adapt);
+ ensure_throws(std::runtime_error, to_json(output, fmts));
+
+ // If the b type does not add a "type" key we should get a runtime_error.
+ fmts = make_fmts(b_derived::json_adapt_no_value, c_derived::json_adapt);
+ ensure_throws(std::runtime_error, to_json(output, fmts));
+
+ // If the c type serializes "type" at all we should get an error, because we expected to insert it ourselves.
+ fmts = make_fmts(b_derived::json_adapt, c_derived::json_adapt_some_value);
+ ensure_throws(std::runtime_error, to_json(output, fmts));
+
+ // Attempting to register the same keyed subtype twice should result in a duplicate_type_error.
+ ensure_throws(duplicate_type_error, make_bad_fmts());
}
TEST(serialization_builder_duplicate_type_actions)