Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions include/cpp_yyjson.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4352,8 +4352,25 @@ namespace yyjson
requires (std::same_as<reader::const_value_ref, Json> || writer::detail::base_of_const_value<Json>)
static auto from_json(const Json& json)
{
// --- Library's own value-owning types (writer value/array/object) ---
// Deep-copy the JSON subtree into a fresh self-owned value via the
// implicit converting constructor (T(json) -> doc_.copy_value). This
// MUST rank ahead of the scalar/string branches: due to constructors
// inherited through `using base::base`, std::constructible_from<value,
// string_view> spuriously evaluates to true (on both MSVC and clang),
// which would otherwise misroute object/array input into the string
// branch and throw "is not constructible from JSON string".
// default_initializable<T> excludes the non-owning _ref view types
// (value_ref/array_ref/object_ref), which have no default ctor and
// cannot be a cast target.
if constexpr (std::default_initializable<T> &&
writer::detail::base_of_value<T> &&
requires(const Json& j) { T(j); })
{
return T(json);
}
// --- Scalar types: compile-time dispatch ---
if constexpr (std::same_as<T, bool>)
else if constexpr (std::same_as<T, bool>)
{
if (json.is_bool())
return *json.as_bool();
Expand Down Expand Up @@ -4403,19 +4420,27 @@ namespace yyjson
throw bad_cast(CPPYYJSON_FMT_NS::format("{} is not constructible from JSON number", type_name<T>()));
}
// --- String-like types ---
else if constexpr (!std::is_aggregate_v<T> && std::constructible_from<T, std::string_view>)
// base_of_value<T> guard is belt-and-suspenders: the deep-copy branch
// above already short-circuits library value types, but exclude them
// here too so the inherited-ctor constructible_from false-positive
// (see comment above) can never misroute a value type into string
// construction, even if the deep-copy branch is somehow not reached.
else if constexpr (!std::is_aggregate_v<T> && !writer::detail::base_of_value<T> &&
std::constructible_from<T, std::string_view>)
{
if (!json.is_string())
throw bad_cast(CPPYYJSON_FMT_NS::format("{} is not constructible from JSON string", type_name<T>()));
return T(*json.as_string());
}
else if constexpr (!std::is_aggregate_v<T> && std::constructible_from<T, std::string>)
else if constexpr (!std::is_aggregate_v<T> && !writer::detail::base_of_value<T> &&
std::constructible_from<T, std::string>)
{
if (!json.is_string())
throw bad_cast(CPPYYJSON_FMT_NS::format("{} is not constructible from JSON string", type_name<T>()));
return T(std::string(*json.as_string()));
}
else if constexpr (!std::is_aggregate_v<T> && std::constructible_from<T, const char*>)
else if constexpr (!std::is_aggregate_v<T> && !writer::detail::base_of_value<T> &&
std::constructible_from<T, const char*>)
{
if (!json.is_string())
throw bad_cast(CPPYYJSON_FMT_NS::format("{} is not constructible from JSON string", type_name<T>()));
Expand Down
5 changes: 5 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ target_link_libraries("${PROJECT_NAME}_test_aggregate_ctor"
PRIVATE ${PROJECT_NAME} GTest::gtest GTest::gtest_main)
gtest_discover_tests("${PROJECT_NAME}_test_aggregate_ctor" DISCOVERY_MODE PRE_TEST)

add_executable("${PROJECT_NAME}_test_value_cast" test_value_cast.cpp)
target_link_libraries("${PROJECT_NAME}_test_value_cast"
PRIVATE ${PROJECT_NAME} GTest::gtest GTest::gtest_main)
gtest_discover_tests("${PROJECT_NAME}_test_value_cast" DISCOVERY_MODE PRE_TEST)


# benchmark
if(CPPYYJSON_BUILD_BENCH)
Expand Down
60 changes: 60 additions & 0 deletions test/test_value_cast.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Regression: cast<yyjson::value> and aggregate reflection of DTOs with
// yyjson::value fields must deep-copy the JSON subtree.
//
// Before the fix, default_caster<yyjson::value> fell into the scalar string
// branch (because std::constructible_from<value, string_view> spuriously
// evaluated to true via inherited constructors), throwing
// "is not constructible from JSON string" for object/array input on MSVC
// (and for direct cast<value> on clang too). The library now deep-copies its
// own value-owning types ahead of the scalar/string branches.
#include <cassert>
#include <cpp_yyjson.hpp>

#include <gtest/gtest.h>
#include <string>
namespace yyjson_value_cast_test
{
struct TaskDto
{
std::string task_name;
std::string entry;
yyjson::value pipeline_override;
};
} // namespace yyjson_value_cast_test

template <>
struct yyjson::field_name_rule<yyjson_value_cast_test::TaskDto>
{
using type = yyjson::snake_to_camel_transform;
};

TEST(ValueCast, DirectObjectSubtreeDeepCopies)
{
auto parsed = yyjson::read(std::string_view(
R"({"taskName":"T","entry":"E","pipelineOverride":{"Stage":{"value":"one"}}})"));
auto v = yyjson::cast<yyjson::value>(parsed);
EXPECT_TRUE(v.is_object());
}

TEST(ValueCast, DirectScalarSubtreeDeepCopies)
{
auto parsed = yyjson::read(std::string_view("42"));
auto v = yyjson::cast<yyjson::value>(parsed);
EXPECT_TRUE(v.is_uint());
}

TEST(ValueCast, AggregateDtoWithValueField)
{
auto parsed = yyjson::read(std::string_view(
R"({"taskName":"T","entry":"E","pipelineOverride":{"Stage":{"value":"one"}}})"));
auto t = yyjson::cast<yyjson_value_cast_test::TaskDto>(parsed);
EXPECT_EQ(t.task_name, "T");
EXPECT_EQ(t.entry, "E");
EXPECT_TRUE(t.pipeline_override.is_object());
}

TEST(ValueCast, StringCastUnaffectedBySfinaeTightening)
{
auto parsed = yyjson::read(std::string_view("\"hello\""));
EXPECT_EQ(yyjson::cast<std::string>(parsed), "hello");
}
Loading