Skip to content

Commit f00247d

Browse files
authored
Avoid crash when converting dict with circular reference (#74)
Fixes #73
1 parent 482c9a2 commit f00247d

File tree

2 files changed

+47
-3
lines changed

2 files changed

+47
-3
lines changed

include/pybind11_json/pybind11_json.hpp

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#ifndef PYBIND11_JSON_HPP
1010
#define PYBIND11_JSON_HPP
1111

12+
#include <set>
1213
#include <string>
1314
#include <vector>
1415

@@ -67,7 +68,7 @@ namespace pyjson
6768
}
6869
}
6970

70-
inline nl::json to_json(const py::handle& obj)
71+
inline nl::json to_json(const py::handle& obj, std::set<const PyObject*>& refs)
7172
{
7273
if (obj.ptr() == nullptr || obj.is_none())
7374
{
@@ -118,24 +119,48 @@ namespace pyjson
118119
}
119120
if (py::isinstance<py::tuple>(obj) || py::isinstance<py::list>(obj))
120121
{
122+
auto insert_ret = refs.insert(obj.ptr());
123+
if (!insert_ret.second) {
124+
throw std::runtime_error("Circular reference detected");
125+
}
126+
121127
auto out = nl::json::array();
122128
for (const py::handle value : obj)
123129
{
124-
out.push_back(to_json(value));
130+
out.push_back(to_json(value, refs));
125131
}
132+
133+
refs.erase(insert_ret.first);
134+
126135
return out;
127136
}
128137
if (py::isinstance<py::dict>(obj))
129138
{
139+
auto insert_ret = refs.insert(obj.ptr());
140+
if (!insert_ret.second) {
141+
throw std::runtime_error("Circular reference detected");
142+
}
143+
130144
auto out = nl::json::object();
131145
for (const py::handle key : obj)
132146
{
133-
out[py::str(key).cast<std::string>()] = to_json(obj[key]);
147+
out[py::str(key).cast<std::string>()] = to_json(obj[key], refs);
134148
}
149+
150+
refs.erase(insert_ret.first);
151+
135152
return out;
136153
}
154+
137155
throw std::runtime_error("to_json not implemented for this type of object: " + py::repr(obj).cast<std::string>());
138156
}
157+
158+
inline nl::json to_json(const py::handle& obj)
159+
{
160+
std::set<const PyObject*> refs;
161+
return to_json(obj, refs);
162+
}
163+
139164
}
140165

141166
// nlohmann_json serializers

test/test_pybind11_json.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,3 +481,22 @@ TEST(pybind11_caster_fromjson, dict)
481481
ASSERT_EQ(j["number"].cast<int>(), 1234);
482482
ASSERT_EQ(j["hello"].cast<std::string>(), "world");
483483
}
484+
485+
TEST(pybind11_caster_tojson, recursive_dict)
486+
{
487+
py::scoped_interpreter guard;
488+
py::module m = create_module("test");
489+
490+
m.def("to_json", &test_fromtojson);
491+
492+
// Simulate calling this binding from Python with a dictionary as argument
493+
py::dict obj_inner("number"_a=1234, "hello"_a="world");
494+
py::dict obj;
495+
obj["first"] = obj_inner;
496+
obj["second"] = obj_inner;
497+
498+
ASSERT_NO_THROW(m.attr("to_json")(obj));
499+
500+
obj["second"]["recur"] = obj_inner;
501+
ASSERT_ANY_THROW(m.attr("to_json")(obj));
502+
}

0 commit comments

Comments
 (0)