Add std::variant JSON serialization infrastructure (issue 150)

- Add #include <variant> header
- Add json_type_for_type() helper for type-based disambiguation
- Add to_json for std::variant that serializes active alternative
- Add variant_try_helper_impl for recursive variant deserialization
- Add json_to_impl specialization for std::variant
- Add TestVariantType (std::variant<std::string, std::vector<FunctionCall>>)
  for testing variant serialization
- Register TestVariantType in json_dispatch_map for -R debug flag
- Add compile-time static_assert for TestVariantType
- Add test_json_variant.sh test suite
- Add test to CMakeLists.txt
This commit is contained in:
2026-06-11 21:13:10 +00:00
parent 4089b68ec4
commit ffcc907a99
3 changed files with 163 additions and 0 deletions
+93
View File
@@ -9,6 +9,7 @@
#include <type_traits>
#include <unordered_map>
#include <functional>
#include <variant>
// Forward declaration of template function for JSON deserialization
// Specialized implementations are generated by DRH_DEFINE_TYPE_* macros
@@ -88,6 +89,78 @@ std::expected<T, std::string> json_to(const nlohmann::json& j) {
return json_to_impl<T>::apply(j);
}
// ============================================================================
// std::variant serialization/deserialization support
// ============================================================================
// Helper to get the JSON type that a type serializes to
// Used for type-based disambiguation during variant deserialization
template<typename T>
constexpr nlohmann::json::value_type json_type_for_type() {
if constexpr (std::is_same_v<std::decay_t<T>, std::string>) {
return nlohmann::json::value_t::string;
} else if constexpr (requires { typename std::decay_t<T>::value_type; }) {
// std::vector<T> or similar container
return nlohmann::json::value_t::array;
} else if constexpr (std::is_integral_v<std::decay_t<T>>) {
return nlohmann::json::value_t::number_integer;
} else if constexpr (std::is_floating_point_v<std::decay_t<T>>) {
return nlohmann::json::value_t::number_float;
} else if constexpr (std::is_same_v<std::decay_t<T>, bool>) {
return nlohmann::json::value_t::boolean;
} else {
// Custom types serialize to objects
return nlohmann::json::value_t::object;
}
}
// to_json for std::variant - serializes the active alternative
template<typename... Alternatives>
void to_json(nlohmann::json& j, const std::variant<Alternatives...>& v) {
std::visit([&j](const auto& value) {
j = value;
}, v);
}
// Helper for recursive variant alternative trying
// Base case: no more alternatives to try
template<typename VariantType, std::size_t CurrentIdx, std::size_t MaxIdx>
struct variant_try_helper;
// Primary template: try current alternative, then recurse
template<typename VariantType, std::size_t CurrentIdx, std::size_t MaxIdx, typename... Alternatives>
struct variant_try_helper_impl {
static std::expected<VariantType, std::string> apply(const nlohmann::json& j, nlohmann::json::value_type json_type) {
using Alt = std::tuple_element_t<CurrentIdx, std::tuple<Alternatives...>>;
if (json_type_for_type<Alt>() == json_type) {
auto result = json_to<Alt>(j);
if (result.has_value()) {
return VariantType{std::in_place_index<CurrentIdx>, std::move(result.value())};
}
// Deserialization failed, but JSON type matched - return error
return std::unexpected("variant alternative " + std::to_string(CurrentIdx) + ": " + result.error());
}
// JSON type doesn't match, try next alternative
if constexpr (CurrentIdx + 1 < MaxIdx) {
return variant_try_helper_impl<VariantType, CurrentIdx + 1, MaxIdx, Alternatives...>::apply(j, json_type);
} else {
return std::unexpected("no variant alternative matched JSON type");
}
}
};
// json_to_impl specialization for std::variant
// Uses type-based disambiguation: matches JSON type to variant alternatives
template<typename... Alternatives>
struct json_to_impl<std::variant<Alternatives...>> {
static std::expected<std::variant<Alternatives...>, std::string> apply(const nlohmann::json& j) {
using VariantType = std::variant<Alternatives...>;
return variant_try_helper_impl<VariantType, 0, sizeof...(Alternatives), Alternatives...>::apply(j, j.type());
}
};
// Helper macros for common registration logic used by all DRH_DEFINE_TYPE_* macros
// Register has_json_to<Type> trait
@@ -238,6 +311,23 @@ inline const auto json_register_vector_FunctionCall = []() {
return 0u;
}();
// Test-only variant type for validating variant serialization/deserialization
// JSON string → std::string, JSON array → std::vector<FunctionCall>
using TestVariantType = std::variant<std::string, std::vector<FunctionCall>>;
// has_json_to specialization for TestVariantType
template<> struct has_json_to<TestVariantType> : std::true_type {};
// Register TestVariantType in json_dispatch_map for -R debug flag
inline const auto drh_register_TestVariantType = []() {
json_dispatch_map["TestVariantType"] = [](const nlohmann::json& j) {
auto result = json_to<TestVariantType>(j);
if (result) std::cout << "success" << std::endl;
else std::cout << "error: " << result.error() << std::endl;
};
return 0u;
}();
struct ToolCall {
size_t index;
std::string id = "";
@@ -408,3 +498,6 @@ DRH_DEFINE_TYPE_MIXED(MixedTypeAllOptionalTest, (), (field1, field2))
static_assert(has_json_to_v<MixedTypeSmokeTest>, "MixedTypeSmokeTest should have json_to specialization");
static_assert(has_json_to_v<MixedTypeAllRequiredTest>, "MixedTypeAllRequiredTest should have json_to specialization");
static_assert(has_json_to_v<MixedTypeAllOptionalTest>, "MixedTypeAllOptionalTest should have json_to specialization");
// Compile-time test for variant type
static_assert(has_json_to_v<TestVariantType>, "TestVariantType should have json_to specialization");
+3
View File
@@ -48,5 +48,8 @@ add_dr_test(test_json_type_mixed)
# Add json_to_field tests
add_dr_test(test_json_to_field)
# Add std::variant JSON serialization tests
add_dr_test(test_json_variant)
# Add expected error handling tests
add_dr_test(test_expected_errors)
+67
View File
@@ -0,0 +1,67 @@
#!/bin/bash
# test_json_variant.sh - Tests for std::variant JSON serialization/deserialization
# Test TestVariantType with string alternative (JSON string)
OUTPUT=$("${DRHARNESS}" -R TestVariantType '"hello world"')
if [ "${OUTPUT}" != "success" ]; then
echo "FAIL: TestVariantType string alternative - expected 'success', got '${OUTPUT}'"
exit 1
fi
# Test TestVariantType with vector alternative (JSON array with valid FunctionCall)
OUTPUT=$("${DRHARNESS}" -R TestVariantType '[{"name": "test", "arguments": "{}"}]')
if [ "${OUTPUT}" != "success" ]; then
echo "FAIL: TestVariantType vector alternative - expected 'success', got '${OUTPUT}'"
exit 1
fi
# Test TestVariantType with empty vector
OUTPUT=$("${DRHARNESS}" -R TestVariantType '[]')
if [ "${OUTPUT}" != "success" ]; then
echo "FAIL: TestVariantType empty vector - expected 'success', got '${OUTPUT}'"
exit 1
fi
# Test TestVariantType with multiple FunctionCalls in vector
OUTPUT=$("${DRHARNESS}" -R TestVariantType '[{"name": "func1", "arguments": "{}"}, {"name": "func2", "arguments": "{}"}]')
if [ "${OUTPUT}" != "success" ]; then
echo "FAIL: TestVariantType multiple items in vector - expected 'success', got '${OUTPUT}'"
exit 1
fi
# Test TestVariantType with invalid JSON type (number - no matching alternative)
OUTPUT=$("${DRHARNESS}" -R TestVariantType '123')
if [ "${OUTPUT}" != "error: no variant alternative matched JSON type" ]; then
echo "FAIL: TestVariantType number type - expected error, got '${OUTPUT}'"
exit 1
fi
# Test TestVariantType with invalid JSON type (object - no matching alternative)
OUTPUT=$("${DRHARNESS}" -R TestVariantType '{"name": "test"}')
if [ "${OUTPUT}" != "error: no variant alternative matched JSON type" ]; then
echo "FAIL: TestVariantType object type - expected error, got '${OUTPUT}'"
exit 1
fi
# Test TestVariantType with invalid JSON type (boolean - no matching alternative)
OUTPUT=$("${DRHARNESS}" -R TestVariantType 'true')
if [ "${OUTPUT}" != "error: no variant alternative matched JSON type" ]; then
echo "FAIL: TestVariantType boolean type - expected error, got '${OUTPUT}'"
exit 1
fi
# Test TestVariantType with empty string (valid string alternative)
OUTPUT=$("${DRHARNESS}" -R TestVariantType '""')
if [ "${OUTPUT}" != "success" ]; then
echo "FAIL: TestVariantType empty string - expected 'success', got '${OUTPUT}'"
exit 1
fi
# Test TestVariantType with vector containing FunctionCall with only optional fields
OUTPUT=$("${DRHARNESS}" -R TestVariantType '[{"name": "test"}]')
if [ "${OUTPUT}" != "success" ]; then
echo "FAIL: TestVariantType partial FunctionCall - expected 'success', got '${OUTPUT}'"
exit 1
fi
echo "All variant tests passed!"