From 095e05835e2bfdb325cc8e0a26ffa0d7c545390d Mon Sep 17 00:00:00 2001 From: Davide Faconti Date: Sat, 24 Oct 2026 16:21:09 +0200 Subject: [PATCH] Allow a custom "__type" name in the JsonExporter lambda converters The addConverter() overloads that take a user lambda always wrote and registered BT::demangle(typeid(T)) as the JSON "__type", e.g. "geometry_msgs::msg::Pose_ >". addConverter() and BT_JSON_CONVERTER already let the user choose the name, but the lambda overloads did not, so such JSON was verbose, compiler-specific, and could not use language-neutral type names. Add an optional trailing type_name parameter (empty keeps the demangled name). The to_json overload writes it in "__type", the from_json overload registers it in the type-name lookup used by fromJson(), in addition to the demangled name, so existing JSON keeps working. A to_json overload taking only (lambda, const char*) forwards with add_type = true; without it, addConverter(f, "Name") would silently convert the string literal to bool add_type. Fixes #877 Co-Authored-By: Claude Opus 5.5 --- include/behaviortree_cpp/json_export.h | 36 +++++++++++++---- tests/gtest_json.cpp | 55 ++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/include/behaviortree_cpp/json_export.h b/include/behaviortree_cpp/json_export.h index af9e11601..129860e8f 100644 --- a/include/behaviortree_cpp/json_export.h +++ b/include/behaviortree_cpp/json_export.h @@ -112,19 +112,33 @@ class JsonExporter * * @param to_json the function with signature void(const T&, nlohmann::json&) * @param add_type if true, add a field called [__type] with the name of the type. + * @param type_name the name stored in [__type]. If empty, the demangled name of T + * is used. Pass the same name to the from_json overload. */ template void addConverter(std::function to_json, - bool add_type = true); + bool add_type = true, const std::string& type_name = {}); + + /// Same as above with add_type = true. Without this overload, a string literal + /// passed as second argument would silently convert to add_type. + template + void addConverter(std::function to_json, + const char* type_name) + { + addConverter(std::move(to_json), true, type_name); + } /** * @brief addConverter register a from_json function that converts a json to a type T. * The conversions from std::vector is automatically registered. * * @param from_json the function with signature void(const nlohmann::json&, T&) + * @param type_name an additional name of T accepted in the field [__type]. The + * demangled name of T is always accepted. */ template - void addConverter(std::function from_json); + void addConverter(std::function from_json, + const std::string& type_name = {}); private: using ToJonConverter = std::function; @@ -196,14 +210,16 @@ inline void JsonExporter::addConverter() } template -inline void JsonExporter::addConverter( - std::function func, bool add_type) +inline void +JsonExporter::addConverter(std::function func, + bool add_type, const std::string& type_name) { - auto converter = [func, add_type](const BT::Any& entry, nlohmann::json& json) { + const std::string name = type_name.empty() ? BT::demangle(typeid(T)) : type_name; + auto converter = [func, add_type, name](const BT::Any& entry, nlohmann::json& json) { func(entry.cast(), json); if(add_type) { - json["__type"] = BT::demangle(typeid(T)); + json["__type"] = name; } }; //--------------------------------------------- @@ -222,8 +238,8 @@ inline void JsonExporter::addConverter( } template -inline void -JsonExporter::addConverter(std::function func) +inline void JsonExporter::addConverter( + std::function func, const std::string& type_name) { auto converter = [func](const nlohmann::json& json) -> Entry { T tmp; @@ -231,6 +247,10 @@ JsonExporter::addConverter(std::function func) return { BT::Any(tmp), BT::TypeInfo::Create() }; }; type_names_.insert({ BT::demangle(typeid(T)), BT::TypeInfo::Create() }); + if(!type_name.empty()) + { + type_names_.insert({ type_name, BT::TypeInfo::Create() }); + } from_json_converters_.insert({ typeid(T), std::move(converter) }); //--------------------------------------------- // add the vector converter diff --git a/tests/gtest_json.cpp b/tests/gtest_json.cpp index da806d70b..be0518888 100644 --- a/tests/gtest_json.cpp +++ b/tests/gtest_json.cpp @@ -48,6 +48,11 @@ struct Time uint32_t nsec; }; +struct Temperature +{ + double celsius = 0; +}; + // NOLINTBEGIN(misc-use-internal-linkage) BT_JSON_CONVERTER(Vector3D, v) { @@ -82,6 +87,16 @@ inline void jsonToTime(const nlohmann::json& j, Time& t) t.sec = static_cast(sec); t.nsec = static_cast((sec - t.sec) * 1e9); } + +inline void jsonFromTemperature(const Temperature& t, nlohmann::json& j) +{ + j["celsius"] = t.celsius; +} + +inline void jsonToTemperature(const nlohmann::json& j, Temperature& t) +{ + t.celsius = j["celsius"]; +} // NOLINTEND(misc-use-internal-linkage) } // namespace TestTypes @@ -177,6 +192,46 @@ TEST_F(JsonTest, CustomTime) } } +// Issue #877: the name stored in "__type" can be chosen when registering lambdas +TEST_F(JsonTest, CustomTypeName) +{ + BT::JsonExporter& exporter = BT::JsonExporter::get(); + const std::string name = "Temperature"; + // a string literal as second argument is the type name, not add_type + using TestTypes::Temperature; + exporter.addConverter(TestTypes::jsonFromTemperature, "Temperature"); + exporter.addConverter(TestTypes::jsonToTemperature, name); + + TestTypes::Temperature temp = { 21.5 }; + nlohmann::json json; + ASSERT_TRUE(exporter.toJson(BT::Any(temp), json)); + ASSERT_EQ(json["__type"], name); + + auto res = exporter.fromJson(json); + ASSERT_TRUE(res) << res.error(); + ASSERT_EQ(res->celsius, 21.5); + + // the demangled name is still accepted + json["__type"] = BT::demangle(typeid(TestTypes::Temperature)); + ASSERT_TRUE(exporter.fromJson(json)); + + // vectors use the same name + std::vector temps = { { 1.5 }, { 2.5 } }; + nlohmann::json json_vec; + ASSERT_TRUE(exporter.toJson(BT::Any(temps), json_vec)); + ASSERT_EQ(json_vec[1]["__type"], name); + + auto res_vec = exporter.fromJson>(json_vec); + ASSERT_TRUE(res_vec) << res_vec.error(); + ASSERT_EQ(res_vec->size(), 2u); + ASSERT_EQ((*res_vec)[1].celsius, 2.5); + + // without a name, the demangled name of the type is used + TestTypes::Time stamp = { 3, 8000000 }; + ASSERT_TRUE(exporter.toJson(BT::Any(stamp), json)); + ASSERT_EQ(json["__type"], BT::demangle(typeid(TestTypes::Time))); +} + TEST_F(JsonTest, ConvertFromString) { TestTypes::Vector3D vect;