Skip to content
Open
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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib; ${CMAKE_INSTALL_PREFIX}/${

set(PYBIND11_JSON_HEADERS
include/pybind11_json/pybind11_json.hpp
include/pybind11_json/pybind11_ordered_json.hpp
)

add_library(${PROJECT_NAME} INTERFACE)
Expand Down
249 changes: 249 additions & 0 deletions include/pybind11_json/pybind11_ordered_json.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/***************************************************************************
* Copyright (c) 2026, Martin Renou, Gracjan Adamus *
* *
* Distributed under the terms of the BSD 3-Clause License. *
* *
* The full license is in the file LICENSE, distributed with this software. *
****************************************************************************/

#ifndef PYBIND11_JSON_HPP
#define PYBIND11_JSON_HPP

#include <set>
#include <string>
#include <vector>

#include "nlohmann/json.hpp"

#include "pybind11/pybind11.h"

namespace py = pybind11;
namespace nl = nlohmann;

namespace pyjson
{
inline py::object from_ordered_json(const nl::ordered_json& j)
{
if (j.is_null())
{
return py::none();
}
else if (j.is_boolean())
{
return py::bool_(j.get<bool>());
}
else if (j.is_number_unsigned())
{
return py::int_(j.get<nl::ordered_json::number_unsigned_t>());
}
else if (j.is_number_integer())
{
return py::int_(j.get<nl::ordered_json::number_integer_t>());
}
else if (j.is_number_float())
{
return py::float_(j.get<double>());
}
else if (j.is_string())
{
return py::str(j.get<std::string>());
}
else if (j.is_array())
{
py::list obj(j.size());
for (std::size_t i = 0; i < j.size(); i++)
{
obj[i] = from_ordered_json(j[i]);
}
return obj;
}
else // Object
{
py::dict obj;
for (nl::ordered_json::const_iterator it = j.cbegin(); it != j.cend(); ++it)
{
obj[py::str(it.key())] = from_ordered_json(it.value());
}
return obj;
}
}

inline nl::ordered_json to_ordered_json(const py::handle& obj, std::set<const PyObject*>& refs)
{
if (obj.ptr() == nullptr || obj.is_none())
{
return nullptr;
}
if (py::isinstance<py::bool_>(obj))
{
return obj.cast<bool>();
}
if (py::isinstance<py::int_>(obj))
{
try
{
nl::ordered_json::number_integer_t s = obj.cast<nl::ordered_json::number_integer_t>();
if (py::int_(s).equal(obj))
{
return s;
}
}
catch (...)
{
}
try
{
nl::ordered_json::number_unsigned_t u = obj.cast<nl::ordered_json::number_unsigned_t>();
if (py::int_(u).equal(obj))
{
return u;
}
}
catch (...)
{
}
throw std::runtime_error("to_ordered_json received an integer out of range for both nl::ordered_json::number_integer_t and nl::ordered_json::number_unsigned_t type: " + py::repr(obj).cast<std::string>());
}
if (py::isinstance<py::float_>(obj))
{
return obj.cast<double>();
}
if (py::isinstance<py::bytes>(obj))
{
py::module base64 = py::module::import("base64");
return base64.attr("b64encode")(obj).attr("decode")("utf-8").cast<std::string>();
}
if (py::isinstance<py::str>(obj))
{
return obj.cast<std::string>();
}
if (py::isinstance<py::tuple>(obj) || py::isinstance<py::list>(obj))
{
auto insert_ret = refs.insert(obj.ptr());
if (!insert_ret.second) {
throw std::runtime_error("Circular reference detected");
}

auto out = nl::ordered_json::array();
for (const py::handle value : obj)
{
out.push_back(to_ordered_json(value, refs));
}

refs.erase(insert_ret.first);

return out;
}
if (py::isinstance<py::dict>(obj))
{
auto insert_ret = refs.insert(obj.ptr());
if (!insert_ret.second) {
throw std::runtime_error("Circular reference detected");
}

auto out = nl::ordered_json::object();
for (const py::handle key : obj)
{
out[py::str(key).cast<std::string>()] = to_ordered_json(obj[key], refs);
}

refs.erase(insert_ret.first);

return out;
}

throw std::runtime_error("to_ordered_json not implemented for this type of object: " + py::repr(obj).cast<std::string>());
}

inline nl::ordered_json to_ordered_json(const py::handle& obj)
{
std::set<const PyObject*> refs;
return to_ordered_json(obj, refs);
}

}

// nlohmann_json serializers
namespace nlohmann
{
#define MAKE_NLJSON_SERIALIZER_DESERIALIZER(T) \
template <> \
struct adl_serializer<T> \
{ \
inline static void to_json(ordered_json& j, const T& obj) \
{ \
j = pyjson::to_ordered_json(obj); \
} \
\
inline static T from_json(const ordered_json& j) \
{ \
return pyjson::from_ordered_json(j); \
} \
}

#define MAKE_NLJSON_SERIALIZER_ONLY(T) \
template <> \
struct adl_serializer<T> \
{ \
inline static void to_json(ordered_json& j, const T& obj) \
{ \
j = pyjson::to_ordered_json(obj); \
} \
}

MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::object);

MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::bool_);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::int_);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::float_);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::str);

MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::list);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::tuple);
MAKE_NLJSON_SERIALIZER_DESERIALIZER(py::dict);

MAKE_NLJSON_SERIALIZER_ONLY(py::handle);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::item_accessor);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::list_accessor);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::tuple_accessor);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::sequence_accessor);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::str_attr_accessor);
MAKE_NLJSON_SERIALIZER_ONLY(py::detail::obj_attr_accessor);

#undef MAKE_NLJSON_SERIALIZER
#undef MAKE_NLJSON_SERIALIZER_ONLY
}

// pybind11 caster
namespace pybind11
{
namespace detail
{
template <> struct type_caster<nl::ordered_json>
{
public:
PYBIND11_TYPE_CASTER(nl::ordered_json, _("ordered_json"));

bool load(handle src, bool)
{
try
{
value = pyjson::to_ordered_json(src);
return true;
}
catch (...)
{
return false;
}
}

static handle cast(nl::ordered_json src, return_value_policy /* policy */, handle /* parent */)
{
object obj = pyjson::from_ordered_json(src);
return obj.release();
}
};
}
}

#endif
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ include_directories(${GTEST_INCLUDE_DIRS} SYSTEM)

set(PYBIND11_JSON_TESTS
test_pybind11_json.cpp
test_pybind11_ordered_json.cpp
)

add_executable(test_pybind11_json ${PYBIND11_JSON_TESTS})
Expand Down
Loading