From 21ffc579ee752b9c08627d97a5c53d3bc22894bd Mon Sep 17 00:00:00 2001 From: Dusk_NM02 Date: Thu, 2 Jul 2026 18:29:23 +0800 Subject: [PATCH] fix(caster): deep-copy library value types ahead of scalar/string dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit default_caster fell into the scalar string branch because constructible_from spuriously evaluated to true on both MSVC and clang (constructors inherited via `using base::base`), throwing "is not constructible from JSON string" for object/array input. This broke DTOs that reflect a yyjson::value field under default_caster (e.g. DAS MaaExecutionPlanDto::tasks[].pipeline_override) on MSVC, and direct cast on every compiler. - default_caster: add a deep-copy branch for library value-owning types (default_initializable && base_of_value && requires{T(json)}) ranked AHEAD of the scalar/string branches; returns T(json), which uses the existing implicit converting ctor (doc_.copy_value) to clone the subtree into a fresh self-owned value. Excludes non-owning _ref views. - Tighten the three string-branch SFINAEs (4406/4412/4418) with `&& !base_of_value` as belt-and-suspenders. - test_value_cast.cpp: regression covering direct cast(object/number), aggregate DTO with value field, and that string cast is unaffected. Verified: DAS DasMaaPiRuntime 20/20 on MSVC (was 13 fail), DAS full DasMaaPiTest 66 pass/0 fail on mingw; lib ctest 63/63 on clang (mingw), including test.cpp 1000+ static_asserts compiling clean. 修复(转换器): 库 value 类型在 scalar/字符串派发之前深拷贝 constructible_from 因继承构造函数在 MSVC/clang 上误报为 true,value 误入字符串分支对 object 输入抛 "not constructible from JSON string", 导致带 yyjson::value 字段的 DTO 在 default_caster 反射下于 MSVC 失败。 GLM-5 代表 Dusk 创建了这个提交 --- include/cpp_yyjson.hpp | 33 +++++++++++++++++++--- test/CMakeLists.txt | 5 ++++ test/test_value_cast.cpp | 60 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 test/test_value_cast.cpp diff --git a/include/cpp_yyjson.hpp b/include/cpp_yyjson.hpp index 0857916..dbcf356 100644 --- a/include/cpp_yyjson.hpp +++ b/include/cpp_yyjson.hpp @@ -4352,8 +4352,25 @@ namespace yyjson requires (std::same_as || writer::detail::base_of_const_value) 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 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 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 && + writer::detail::base_of_value && + requires(const Json& j) { T(j); }) + { + return T(json); + } // --- Scalar types: compile-time dispatch --- - if constexpr (std::same_as) + else if constexpr (std::same_as) { if (json.is_bool()) return *json.as_bool(); @@ -4403,19 +4420,27 @@ namespace yyjson throw bad_cast(CPPYYJSON_FMT_NS::format("{} is not constructible from JSON number", type_name())); } // --- String-like types --- - else if constexpr (!std::is_aggregate_v && std::constructible_from) + // base_of_value 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 && !writer::detail::base_of_value && + std::constructible_from) { if (!json.is_string()) throw bad_cast(CPPYYJSON_FMT_NS::format("{} is not constructible from JSON string", type_name())); return T(*json.as_string()); } - else if constexpr (!std::is_aggregate_v && std::constructible_from) + else if constexpr (!std::is_aggregate_v && !writer::detail::base_of_value && + std::constructible_from) { if (!json.is_string()) throw bad_cast(CPPYYJSON_FMT_NS::format("{} is not constructible from JSON string", type_name())); return T(std::string(*json.as_string())); } - else if constexpr (!std::is_aggregate_v && std::constructible_from) + else if constexpr (!std::is_aggregate_v && !writer::detail::base_of_value && + std::constructible_from) { if (!json.is_string()) throw bad_cast(CPPYYJSON_FMT_NS::format("{} is not constructible from JSON string", type_name())); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d68fe0f..67af9e4 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -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) diff --git a/test/test_value_cast.cpp b/test/test_value_cast.cpp new file mode 100644 index 0000000..5ce1ea2 --- /dev/null +++ b/test/test_value_cast.cpp @@ -0,0 +1,60 @@ +// Regression: cast and aggregate reflection of DTOs with +// yyjson::value fields must deep-copy the JSON subtree. +// +// Before the fix, default_caster fell into the scalar string +// branch (because std::constructible_from spuriously +// evaluated to true via inherited constructors), throwing +// "is not constructible from JSON string" for object/array input on MSVC +// (and for direct cast on clang too). The library now deep-copies its +// own value-owning types ahead of the scalar/string branches. +#include +#include + +#include +#include +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 +{ + 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(parsed); + EXPECT_TRUE(v.is_object()); +} + +TEST(ValueCast, DirectScalarSubtreeDeepCopies) +{ + auto parsed = yyjson::read(std::string_view("42")); + auto v = yyjson::cast(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(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(parsed), "hello"); +}