Compare commits

...
Author SHA1 Message Date
Niels Lohmann 8952057295 Use the with_object_t alias for the custom object key test types
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-09 14:41:39 +02:00
Niels Lohmann 0db359cabc Restore v3.12.0 support for custom object key types
Custom object_t types whose key_type is not string_t compiled with
v3.12.0 for several APIs that unreleased changes broke:

- to_bson failed for every custom key type (#5553 kept a const string_t*
  to the key); the nested entry's header is now written where the entry
  is found.
- Copying deep values (and parse, merge_patch, update, insert) required
  operator== on keys (#5389); keys without one are now paired via find().
- to_cbor/to_msgpack required an implicit conversion to string_t (#5746,
  #5328); keys without one go through a temporary basic_json again.
- at() required a conversion to string_t for its error message (#5727);
  other keys are passed to concat() unchanged again.

The new unit-custom-object-key-type.cpp covers five key types with
different capabilities.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-09 14:37:07 +02:00
Alex Prabhat Bara 69a0c1b82c Avoid allocating temporary basic_json for cbor and msgpack object keys (#5328)
* avoid allocating temporary basic_json for CBOR and MessagePack object keys

Signed-off-by: alexprabhat99 <alexpbara@gmail.com>

* add size() to the custom object key test type

UBJSON and BJData access object keys through size() and c_str()
directly, so the key type now provides both and the comment says why.

Signed-off-by: alexprabhat99 <alexpbara@gmail.com>

* address review: drop key size()/c_str(), test keys below the depth limit

Nothing in the library calls size() or c_str() on an object key, so the
test key type only keeps data(), which JSON_DIAGNOSTICS needs.

The CBOR and MessagePack custom key tests now also nest objects deeper
than detail::recursion_depth_limit(), so keys written by
write_cbor_iterative and write_msgpack_iterative are covered as well.

Signed-off-by: alexprabhat99 <alexpbara@gmail.com>

---------

Signed-off-by: alexprabhat99 <alexpbara@gmail.com>
2026-10-09 13:22:07 +02:00
Niels Lohmann d33068da73 Address review comments on the API stability docs and a test comment (#5784)
* Address review comments on #5775 and #5779

Allow new defaulted parameters and new default arguments in the API
stability rules, mention the macro opt-in, and drop the redundant
recompile advice. Describe test-diagnostics-optimized as the regression
test for the fixed #5742.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

* Document what counts as a breaking change in the API stability rules

Spell out the 3.x compatibility rules in the roadmap: new defaulted
parameters, new default arguments, noexcept/constexpr, template
parameters, parse and dump results, accepted input, key iteration order,
iterator invalidation, implicit conversions, to_json/from_json lookup,
json_sax, value_t enumerators, and documented macros, CMake options and
headers. Also list std::hash values as not part of the public API, and
link the macro overview from the section.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-08 17:44:34 +02:00
Niels Lohmann d8dfc0d0f9 Fix unused-result warnings in the contains and parse_error examples (#5788)
contains(json_pointer) is marked JSON_HEDLEY_WARN_UNUSED_RESULT since #5477,
and parse() has been for longer. The contains example ignored the result in
two try blocks waiting for a parse_error that contains() never throws, so
they printed nothing; print the result for those pointers instead.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-10-08 16:21:59 +02:00
13 changed files with 1063 additions and 196 deletions

No files matched your search

+23 -9
View File
@@ -36,13 +36,26 @@ work items are tracked in the [GitHub milestones](https://github.com/nlohmann/js
## API stability
Releases follow [semantic versioning](https://semver.org): a minor or patch release of version 3.x does not break code
that uses the public API. In particular, a 3.x release does not:
that uses the public API, unless that code opts in to a change with a macro as described [below](#version-40). In
particular, a 3.x release does not:
- change the signature of a function (its parameter types, return type, number of parameters, or the const-ness of a
member function);
- remove or rename a function or class;
- make breaking changes to the signature of a function: the types or order of its existing parameters, its return type,
its `noexcept` or `constexpr` specifier, or the const-ness of a member function. New parameters may be added if they
have a default value;
- remove or rename a function or class, or change the template parameters of a public class template;
- change which exceptions a function throws, or the [exception ids](../home/exceptions.md);
- change access specifiers or default arguments.
- change access specifiers, or change or remove existing default arguments. New default arguments may be added;
- change the JSON type that a valid input parses to, or the text that `dump()` produces for a valid value;
- accept input that was rejected before, or reject input that was accepted before;
- change the order in which the keys of an object are iterated. The default type sorts keys, and
[`ordered_json`](../api/ordered_json.md) keeps insertion order;
- change when iterators, pointers, or references are invalidated, or the state of a moved-from `basic_json`;
- add or remove implicit conversions from `basic_json`;
- change how `to_json` and `from_json` functions are found, or the behavior of
[`adl_serializer`](../api/adl_serializer/index.md);
- add pure virtual functions to the [`json_sax`](../api/json_sax/index.md) interface;
- remove, rename, renumber, or add enumerators of `value_t`;
- remove or rename a documented macro, CMake option, CMake target, or header, or change what a documented macro does.
Exceptions to these rules, for instance when fixing a bug requires changing the exception a function throws, are
documented in the [release notes](../home/releases.md).
@@ -51,13 +64,14 @@ The following are **not** part of the public API and may change in any release,
- The text of exception messages returned by `what()`. Use the [exception id](../home/exceptions.md) to tell errors
apart.
- The ABI, including `sizeof(basic_json)` and the memory layout of its values. Recompile your code when you upgrade the
library. The [versioned inline namespace](../features/namespace.md) turns mixing versions into a link error.
- The ABI, including `sizeof(basic_json)` and the memory layout of its values. The
[versioned inline namespace](../features/namespace.md) turns mixing versions into a link error.
- The hash values returned by `std::hash` for `basic_json`. Numbers that compare equal still hash equally.
- Everything in namespace `nlohmann::detail`, and macros and type traits that are not documented in the
[API reference](../api/basic_json/index.md).
Changes that would break the public API are only added behind a macro whose default keeps the 3.x behavior, see
[Version 4.0](#version-40).
Breaking changes are only added behind a macro whose default keeps the 3.x behavior. See [Version 4.0](#version-40) and
the [macro overview](../features/macros.md).
## Version 4.0
@@ -19,25 +19,9 @@ int main()
<< j.contains("/array/1"_json_pointer) << '\n'
<< j.contains("/array/-"_json_pointer) << '\n'
<< j.contains("/array/4"_json_pointer) << '\n'
<< j.contains("/baz"_json_pointer) << std::endl;
try
{
// try to use an array index with leading '0'
j.contains("/array/01"_json_pointer);
}
catch (const json::parse_error& e)
{
std::cout << e.what() << '\n';
}
try
{
// try to use an array index that is not a number
j.contains("/array/one"_json_pointer);
}
catch (const json::parse_error& e)
{
std::cout << e.what() << '\n';
}
<< j.contains("/baz"_json_pointer) << '\n'
// an array index with a leading '0' is not found
<< j.contains("/array/01"_json_pointer) << '\n'
// an array index that is not a number is not found
<< j.contains("/array/one"_json_pointer) << std::endl;
}
@@ -5,3 +5,5 @@ true
false
false
false
false
false
+1 -1
View File
@@ -8,7 +8,7 @@ int main()
try
{
// parsing input with a syntax error
json::parse("[1,2,3,]");
json j = json::parse("[1,2,3,]");
}
catch (const json::parse_error& e)
{
@@ -172,6 +172,10 @@ struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json<T>::value >>
template<typename T>
using detect_key_compare = typename T::key_compare;
// detects whether two values of type T can be compared with operator==
template<typename T>
using detect_equal_comparable = decltype(static_cast<bool>(std::declval<const T&>() == std::declval<const T&>()));
// obtains the actual object key comparator: object_t::key_compare if the
// object type defines it, and default_object_comparator_t otherwise
//
+134 -80
View File
@@ -244,16 +244,7 @@ class binary_writer
case value_t::string:
{
string_t storage;
const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage);
// step 1: write control byte and the string length
write_cbor_head(0x60, value.size());
// step 2: write the string
oa.write_characters(
reinterpret_cast<const CharType*>(value.data()),
value.size());
write_cbor_string(*j.m_data.m_value.string, j);
break;
}
@@ -321,18 +312,9 @@ class binary_writer
for (const auto& el : *j.m_data.m_value.object)
{
// el.first is checked here, against the object as
// diagnostics context, because write_cbor(el.first)
// converts it to a temporary basic_json that would be
// used as the context instead; for error_handler_t::keep
// and ::replace/::ignore the recursive write_cbor(el.first)
// call below handles the key like any other string, so no
// separate check is needed here for those
if (error_handler == error_handler_t::strict)
{
check_utf8(el.first, j);
}
write_cbor(el.first);
// el.first is written directly (not via a temporary
// basic_json), with the object as diagnostics context
write_cbor_key(el.first, j);
write_cbor(el.second, depth + 1);
}
break;
@@ -491,39 +473,7 @@ class binary_writer
case value_t::string:
{
string_t storage;
const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage);
// step 1: write control byte and the string length
const auto N = to_msgpack_length(value.size(), j);
if (N <= 31)
{
// fixstr
write_number(static_cast<std::uint8_t>(0xA0 | N));
}
else if (N <= (std::numeric_limits<std::uint8_t>::max)())
{
// str 8
oa.write_character(to_char_type(0xD9));
write_number(static_cast<std::uint8_t>(N));
}
else if (N <= (std::numeric_limits<std::uint16_t>::max)())
{
// str 16
oa.write_character(to_char_type(0xDA));
write_number(static_cast<std::uint16_t>(N));
}
else
{
// str 32
oa.write_character(to_char_type(0xDB));
write_number(static_cast<std::uint32_t>(N));
}
// step 2: write the string
oa.write_characters(
reinterpret_cast<const CharType*>(value.data()),
value.size());
write_msgpack_string(*j.m_data.m_value.string, j);
break;
}
@@ -634,14 +584,9 @@ class binary_writer
for (const auto& el : *j.m_data.m_value.object)
{
// as in write_cbor, el.first is checked here against the
// object as diagnostics context; the recursive call below
// handles keep/replace/ignore like any other string
if (error_handler == error_handler_t::strict)
{
check_utf8(el.first, j);
}
write_msgpack(el.first);
// as in write_cbor, el.first is written directly with the
// object as diagnostics context
write_msgpack_key(el.first, j);
write_msgpack(el.second, depth + 1);
}
break;
@@ -1025,13 +970,9 @@ class binary_writer
continue;
}
// el.first is checked here, against the object as diagnostics
// context, like the matching check in write_cbor's object case
if (error_handler == error_handler_t::strict)
{
check_utf8(current.object_it->first, *current.value);
}
write_cbor(current.object_it->first);
// the key is written directly (not via a temporary basic_json),
// with the object as diagnostics context, as in write_cbor
write_cbor_key(current.object_it->first, *current.value);
const BasicJsonType* child = &(current.object_it->second);
++stack.back().object_it;
write_cbor_value_or_push(*child, stack);
@@ -1106,11 +1047,9 @@ class binary_writer
continue;
}
if (error_handler == error_handler_t::strict)
{
check_utf8(current.object_it->first, *current.value);
}
write_msgpack(current.object_it->first);
// as in write_cbor_iterative, the key is written directly with
// the object as diagnostics context
write_msgpack_key(current.object_it->first, *current.value);
const BasicJsonType* child = &(current.object_it->second);
++stack.back().object_it;
write_msgpack_value_or_push(*child, stack);
@@ -1877,7 +1816,6 @@ class binary_writer
{
// write entries until the current object or array is done, or an
// entry is an object or array itself
const string_t* nested_name = nullptr;
const BasicJsonType* nested = nullptr;
if (current.value->is_object())
{
@@ -1888,7 +1826,8 @@ class binary_writer
++current.member;
if (el.second.is_structured())
{
nested_name = &el.first;
write_bson_entry_header(el.first, el.second.is_object() ? 0x03 : 0x04);
write_number<std::int32_t>(to_bson_length(nested_sizes[next_size++]), true);
nested = &el.second;
}
else
@@ -1907,7 +1846,8 @@ class binary_writer
++current.index;
if (el.is_structured())
{
nested_name = &index_name;
write_bson_entry_header(index_name, el.is_object() ? 0x03 : 0x04);
write_number<std::int32_t>(to_bson_length(nested_sizes[next_size++]), true);
nested = &el;
}
else
@@ -1919,8 +1859,6 @@ class binary_writer
if (nested != nullptr)
{
write_bson_entry_header(*nested_name, nested->is_object() ? 0x03 : 0x04);
write_number<std::int32_t>(to_bson_length(nested_sizes[next_size++]), true);
parents.push_back(std::move(current));
current = bson_frame(nested);
continue;
@@ -1988,6 +1926,122 @@ class binary_writer
}
}
/*!
@brief write an object key as a CBOR text string
A key convertible to string_t is written directly. Other key types (only
an explicit conversion, or only a to_json overload) go through a temporary
basic_json, as in version 3.12.0; the temporary is then the diagnostics
context for strict UTF-8 checks.
*/
template<typename Key = typename BasicJsonType::object_t::key_type,
enable_if_t<std::is_convertible<Key, string_t>::value, int> = 0>
void write_cbor_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context)
{
write_cbor_string(key, context);
}
template < typename Key = typename BasicJsonType::object_t::key_type,
enable_if_t < !std::is_convertible<Key, string_t>::value, int > = 0 >
void write_cbor_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& /*context*/)
{
write_cbor(BasicJsonType(key));
}
/// @brief write an object key as a MessagePack str, as in @ref write_cbor_key
template<typename Key = typename BasicJsonType::object_t::key_type,
enable_if_t<std::is_convertible<Key, string_t>::value, int> = 0>
void write_msgpack_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context)
{
write_msgpack_string(key, context);
}
template < typename Key = typename BasicJsonType::object_t::key_type,
enable_if_t < !std::is_convertible<Key, string_t>::value, int > = 0 >
void write_msgpack_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& /*context*/)
{
write_msgpack(BasicJsonType(key));
}
/*!
@brief write a CBOR text string
@a value is checked or sanitized according to @ref error_handler, with
@a context (the string value itself, or the object a key belongs to) used
as diagnostics context; this avoids converting object keys to a temporary
basic_json just to write them
@note When object_t::key_type is not string_t, @a value is a temporary
string_t converted from the key, which lives only until the end of
the caller's statement. The reference returned by
@ref sanitize_utf8_for_write may refer to it, so it must not escape
this function.
*/
void write_cbor_string(const string_t& value, const BasicJsonType& context)
{
string_t storage;
const string_t& sanitized = sanitize_utf8_for_write(value, context, storage);
// step 1: write control byte and the string length
write_cbor_head(0x60, sanitized.size());
// step 2: write the string
oa.write_characters(
reinterpret_cast<const CharType*>(sanitized.data()),
sanitized.size());
}
/////////////
// MsgPack //
/////////////
/*!
@brief write a MessagePack str
@a value is checked or sanitized according to @ref error_handler, with
@a context used as diagnostics context, as in @ref write_cbor_string
@note As in @ref write_cbor_string, @a value may be a temporary string_t
converted from a key, so the reference returned by
@ref sanitize_utf8_for_write must not escape this function.
*/
void write_msgpack_string(const string_t& value, const BasicJsonType& context)
{
string_t storage;
const string_t& sanitized = sanitize_utf8_for_write(value, context, storage);
// step 1: write control byte and the string length
const auto N = to_msgpack_length(sanitized.size(), context);
if (N <= 31)
{
// fixstr
write_number(static_cast<std::uint8_t>(0xA0 | N));
}
else if (N <= (std::numeric_limits<std::uint8_t>::max)())
{
// str 8
oa.write_character(to_char_type(0xD9));
write_number(static_cast<std::uint8_t>(N));
}
else if (N <= (std::numeric_limits<std::uint16_t>::max)())
{
// str 16
oa.write_character(to_char_type(0xDA));
write_number(static_cast<std::uint16_t>(N));
}
else
{
// str 32
oa.write_character(to_char_type(0xDB));
write_number(static_cast<std::uint32_t>(N));
}
// step 2: write the string
oa.write_characters(
reinterpret_cast<const CharType*>(sanitized.data()),
sanitized.size());
}
////////////
// UBJSON //
////////////
+36 -2
View File
@@ -1421,6 +1421,24 @@ public:
return create<object_t>(first, last);
}
/// @brief compare two object keys for equality, if the key type supports it
/// @note object_t only needs operator< for its keys (std::map), so operator==
/// may not exist; the keys are then reported as different, which makes
/// copy_object_level pair the values via object_t::find()
template<typename K = typename object_t::key_type,
detail::enable_if_t<detail::is_detected<detail::detect_equal_comparable, K>::value, int> = 0>
static bool copy_keys_equal(const K& a, const K& b)
{
return a == b;
}
template < typename K = typename object_t::key_type,
detail::enable_if_t < !detail::is_detected<detail::detect_equal_comparable, K>::value, int > = 0 >
static bool copy_keys_equal(const K& /*a*/, const K& /*b*/)
{
return false;
}
/// @brief create the copy of the object @a src in @a dst
/// @note structured values are appended to @a worklist instead
static void copy_object_level(const basic_json& src, basic_json& dst,
@@ -1453,7 +1471,7 @@ public:
auto src_it = src_object.cbegin();
for (auto& element : *dst.m_data.m_value.object)
{
if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && src_it->first == element.first))
if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && copy_keys_equal(src_it->first, element.first)))
{
copy_shallow(src_it->second, element.second, worklist);
++src_it;
@@ -3330,11 +3348,27 @@ public:
// std::map or ordered_map) never moves from its argument, so key is still
// valid here regardless of whether KeyType was deduced as an rvalue reference
// NOLINTNEXTLINE(bugprone-use-after-move,hicpp-invalid-access-moved)
JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(key), "' not found"), &j));
JSON_THROW(out_of_range::create(403, detail::concat("key '", key_for_message(key), "' not found"), &j));
}
return it->second;
}
/// @brief key as it is passed to detail::concat for an error message
/// @note string_t is used where it can be constructed from the key; other
/// key types are passed through unchanged, as concat only needs
/// data() and size() of them
template<typename KeyType, detail::enable_if_t<std::is_constructible<string_t, const KeyType&>::value, int> = 0>
static string_t key_for_message(const KeyType& key)
{
return string_t(key);
}
template < typename KeyType, detail::enable_if_t < !std::is_constructible<string_t, const KeyType&>::value, int > = 0 >
static const KeyType & key_for_message(const KeyType& key)
{
return key;
}
/// @brief checked array element access used by the at() overloads taking an index
/// @throw type_error.304 if @a j is not an array
/// @throw out_of_range.401 if @a idx is out of range
+174 -82
View File
@@ -4171,6 +4171,10 @@ struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json<T>::value >>
template<typename T>
using detect_key_compare = typename T::key_compare;
// detects whether two values of type T can be compared with operator==
template<typename T>
using detect_equal_comparable = decltype(static_cast<bool>(std::declval<const T&>() == std::declval<const T&>()));
// obtains the actual object key comparator: object_t::key_compare if the
// object type defines it, and default_object_comparator_t otherwise
//
@@ -21749,16 +21753,7 @@ class binary_writer
case value_t::string:
{
string_t storage;
const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage);
// step 1: write control byte and the string length
write_cbor_head(0x60, value.size());
// step 2: write the string
oa.write_characters(
reinterpret_cast<const CharType*>(value.data()),
value.size());
write_cbor_string(*j.m_data.m_value.string, j);
break;
}
@@ -21826,18 +21821,9 @@ class binary_writer
for (const auto& el : *j.m_data.m_value.object)
{
// el.first is checked here, against the object as
// diagnostics context, because write_cbor(el.first)
// converts it to a temporary basic_json that would be
// used as the context instead; for error_handler_t::keep
// and ::replace/::ignore the recursive write_cbor(el.first)
// call below handles the key like any other string, so no
// separate check is needed here for those
if (error_handler == error_handler_t::strict)
{
check_utf8(el.first, j);
}
write_cbor(el.first);
// el.first is written directly (not via a temporary
// basic_json), with the object as diagnostics context
write_cbor_key(el.first, j);
write_cbor(el.second, depth + 1);
}
break;
@@ -21996,39 +21982,7 @@ class binary_writer
case value_t::string:
{
string_t storage;
const string_t& value = sanitize_utf8_for_write(*j.m_data.m_value.string, j, storage);
// step 1: write control byte and the string length
const auto N = to_msgpack_length(value.size(), j);
if (N <= 31)
{
// fixstr
write_number(static_cast<std::uint8_t>(0xA0 | N));
}
else if (N <= (std::numeric_limits<std::uint8_t>::max)())
{
// str 8
oa.write_character(to_char_type(0xD9));
write_number(static_cast<std::uint8_t>(N));
}
else if (N <= (std::numeric_limits<std::uint16_t>::max)())
{
// str 16
oa.write_character(to_char_type(0xDA));
write_number(static_cast<std::uint16_t>(N));
}
else
{
// str 32
oa.write_character(to_char_type(0xDB));
write_number(static_cast<std::uint32_t>(N));
}
// step 2: write the string
oa.write_characters(
reinterpret_cast<const CharType*>(value.data()),
value.size());
write_msgpack_string(*j.m_data.m_value.string, j);
break;
}
@@ -22139,14 +22093,9 @@ class binary_writer
for (const auto& el : *j.m_data.m_value.object)
{
// as in write_cbor, el.first is checked here against the
// object as diagnostics context; the recursive call below
// handles keep/replace/ignore like any other string
if (error_handler == error_handler_t::strict)
{
check_utf8(el.first, j);
}
write_msgpack(el.first);
// as in write_cbor, el.first is written directly with the
// object as diagnostics context
write_msgpack_key(el.first, j);
write_msgpack(el.second, depth + 1);
}
break;
@@ -22530,13 +22479,9 @@ class binary_writer
continue;
}
// el.first is checked here, against the object as diagnostics
// context, like the matching check in write_cbor's object case
if (error_handler == error_handler_t::strict)
{
check_utf8(current.object_it->first, *current.value);
}
write_cbor(current.object_it->first);
// the key is written directly (not via a temporary basic_json),
// with the object as diagnostics context, as in write_cbor
write_cbor_key(current.object_it->first, *current.value);
const BasicJsonType* child = &(current.object_it->second);
++stack.back().object_it;
write_cbor_value_or_push(*child, stack);
@@ -22611,11 +22556,9 @@ class binary_writer
continue;
}
if (error_handler == error_handler_t::strict)
{
check_utf8(current.object_it->first, *current.value);
}
write_msgpack(current.object_it->first);
// as in write_cbor_iterative, the key is written directly with
// the object as diagnostics context
write_msgpack_key(current.object_it->first, *current.value);
const BasicJsonType* child = &(current.object_it->second);
++stack.back().object_it;
write_msgpack_value_or_push(*child, stack);
@@ -23382,7 +23325,6 @@ class binary_writer
{
// write entries until the current object or array is done, or an
// entry is an object or array itself
const string_t* nested_name = nullptr;
const BasicJsonType* nested = nullptr;
if (current.value->is_object())
{
@@ -23393,7 +23335,8 @@ class binary_writer
++current.member;
if (el.second.is_structured())
{
nested_name = &el.first;
write_bson_entry_header(el.first, el.second.is_object() ? 0x03 : 0x04);
write_number<std::int32_t>(to_bson_length(nested_sizes[next_size++]), true);
nested = &el.second;
}
else
@@ -23412,7 +23355,8 @@ class binary_writer
++current.index;
if (el.is_structured())
{
nested_name = &index_name;
write_bson_entry_header(index_name, el.is_object() ? 0x03 : 0x04);
write_number<std::int32_t>(to_bson_length(nested_sizes[next_size++]), true);
nested = &el;
}
else
@@ -23424,8 +23368,6 @@ class binary_writer
if (nested != nullptr)
{
write_bson_entry_header(*nested_name, nested->is_object() ? 0x03 : 0x04);
write_number<std::int32_t>(to_bson_length(nested_sizes[next_size++]), true);
parents.push_back(std::move(current));
current = bson_frame(nested);
continue;
@@ -23493,6 +23435,122 @@ class binary_writer
}
}
/*!
@brief write an object key as a CBOR text string
A key convertible to string_t is written directly. Other key types (only
an explicit conversion, or only a to_json overload) go through a temporary
basic_json, as in version 3.12.0; the temporary is then the diagnostics
context for strict UTF-8 checks.
*/
template<typename Key = typename BasicJsonType::object_t::key_type,
enable_if_t<std::is_convertible<Key, string_t>::value, int> = 0>
void write_cbor_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context)
{
write_cbor_string(key, context);
}
template < typename Key = typename BasicJsonType::object_t::key_type,
enable_if_t < !std::is_convertible<Key, string_t>::value, int > = 0 >
void write_cbor_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& /*context*/)
{
write_cbor(BasicJsonType(key));
}
/// @brief write an object key as a MessagePack str, as in @ref write_cbor_key
template<typename Key = typename BasicJsonType::object_t::key_type,
enable_if_t<std::is_convertible<Key, string_t>::value, int> = 0>
void write_msgpack_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context)
{
write_msgpack_string(key, context);
}
template < typename Key = typename BasicJsonType::object_t::key_type,
enable_if_t < !std::is_convertible<Key, string_t>::value, int > = 0 >
void write_msgpack_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& /*context*/)
{
write_msgpack(BasicJsonType(key));
}
/*!
@brief write a CBOR text string
@a value is checked or sanitized according to @ref error_handler, with
@a context (the string value itself, or the object a key belongs to) used
as diagnostics context; this avoids converting object keys to a temporary
basic_json just to write them
@note When object_t::key_type is not string_t, @a value is a temporary
string_t converted from the key, which lives only until the end of
the caller's statement. The reference returned by
@ref sanitize_utf8_for_write may refer to it, so it must not escape
this function.
*/
void write_cbor_string(const string_t& value, const BasicJsonType& context)
{
string_t storage;
const string_t& sanitized = sanitize_utf8_for_write(value, context, storage);
// step 1: write control byte and the string length
write_cbor_head(0x60, sanitized.size());
// step 2: write the string
oa.write_characters(
reinterpret_cast<const CharType*>(sanitized.data()),
sanitized.size());
}
/////////////
// MsgPack //
/////////////
/*!
@brief write a MessagePack str
@a value is checked or sanitized according to @ref error_handler, with
@a context used as diagnostics context, as in @ref write_cbor_string
@note As in @ref write_cbor_string, @a value may be a temporary string_t
converted from a key, so the reference returned by
@ref sanitize_utf8_for_write must not escape this function.
*/
void write_msgpack_string(const string_t& value, const BasicJsonType& context)
{
string_t storage;
const string_t& sanitized = sanitize_utf8_for_write(value, context, storage);
// step 1: write control byte and the string length
const auto N = to_msgpack_length(sanitized.size(), context);
if (N <= 31)
{
// fixstr
write_number(static_cast<std::uint8_t>(0xA0 | N));
}
else if (N <= (std::numeric_limits<std::uint8_t>::max)())
{
// str 8
oa.write_character(to_char_type(0xD9));
write_number(static_cast<std::uint8_t>(N));
}
else if (N <= (std::numeric_limits<std::uint16_t>::max)())
{
// str 16
oa.write_character(to_char_type(0xDA));
write_number(static_cast<std::uint16_t>(N));
}
else
{
// str 32
oa.write_character(to_char_type(0xDB));
write_number(static_cast<std::uint32_t>(N));
}
// step 2: write the string
oa.write_characters(
reinterpret_cast<const CharType*>(sanitized.data()),
sanitized.size());
}
////////////
// UBJSON //
////////////
@@ -29130,6 +29188,24 @@ public:
return create<object_t>(first, last);
}
/// @brief compare two object keys for equality, if the key type supports it
/// @note object_t only needs operator< for its keys (std::map), so operator==
/// may not exist; the keys are then reported as different, which makes
/// copy_object_level pair the values via object_t::find()
template<typename K = typename object_t::key_type,
detail::enable_if_t<detail::is_detected<detail::detect_equal_comparable, K>::value, int> = 0>
static bool copy_keys_equal(const K& a, const K& b)
{
return a == b;
}
template < typename K = typename object_t::key_type,
detail::enable_if_t < !detail::is_detected<detail::detect_equal_comparable, K>::value, int > = 0 >
static bool copy_keys_equal(const K& /*a*/, const K& /*b*/)
{
return false;
}
/// @brief create the copy of the object @a src in @a dst
/// @note structured values are appended to @a worklist instead
static void copy_object_level(const basic_json& src, basic_json& dst,
@@ -29162,7 +29238,7 @@ public:
auto src_it = src_object.cbegin();
for (auto& element : *dst.m_data.m_value.object)
{
if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && src_it->first == element.first))
if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && copy_keys_equal(src_it->first, element.first)))
{
copy_shallow(src_it->second, element.second, worklist);
++src_it;
@@ -31039,11 +31115,27 @@ public:
// std::map or ordered_map) never moves from its argument, so key is still
// valid here regardless of whether KeyType was deduced as an rvalue reference
// NOLINTNEXTLINE(bugprone-use-after-move,hicpp-invalid-access-moved)
JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(key), "' not found"), &j));
JSON_THROW(out_of_range::create(403, detail::concat("key '", key_for_message(key), "' not found"), &j));
}
return it->second;
}
/// @brief key as it is passed to detail::concat for an error message
/// @note string_t is used where it can be constructed from the key; other
/// key types are passed through unchanged, as concat only needs
/// data() and size() of them
template<typename KeyType, detail::enable_if_t<std::is_constructible<string_t, const KeyType&>::value, int> = 0>
static string_t key_for_message(const KeyType& key)
{
return string_t(key);
}
template < typename KeyType, detail::enable_if_t < !std::is_constructible<string_t, const KeyType&>::value, int > = 0 >
static const KeyType & key_for_message(const KeyType& key)
{
return key;
}
/// @brief checked array element access used by the at() overloads taking an index
/// @throw type_error.304 if @a j is not an array
/// @throw out_of_range.401 if @a idx is out of range
+2 -1
View File
@@ -138,7 +138,8 @@ json_test_set_test_options(test-disabled_exceptions
# only the #972 regression test needs thirdparty/fifo_map on its include path
json_test_set_test_options(test-regression1 LINK_LIBRARIES fifo_map_include)
# GCC's false -Warray-bounds error with JSON_DIAGNOSTICS only shows up when optimizing (#5742).
# Regression test for GCC's false -Warray-bounds error with JSON_DIAGNOSTICS (#5742, fixed in #5585). It only
# showed up when optimizing, so build this test with -O3 and the warning as an error.
# -O3 makes the optimizer-driven warnings of the ci_test_gcc flag set (-Winline,
# -Wsuggest-attribute=...) fire on the library's inline functions; they are not
# what this test checks, so turn them off for it.
+75
View File
@@ -0,0 +1,75 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#pragma once
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <nlohmann/json.hpp>
namespace custom_object_key_test
{
class key
{
public:
key() = default;
key(const char* value)
: m_value(value)
{}
key(std::string value)
: m_value(std::move(value))
{}
operator std::string() const
{
return m_value;
}
// Required by JSON_DIAGNOSTICS, which reads object keys through data()
// when building the path of an exception.
const char* data() const noexcept
{
return m_value.data();
}
friend bool operator<(const key& lhs, const key& rhs)
{
return lhs.m_value < rhs.m_value;
}
private:
std::string m_value;
};
template<typename Key, typename Value, typename Compare, typename Allocator>
class object
: public std::map <
key,
Value,
std::less<key>, // NOLINT(modernize-use-transparent-functors)
typename std::allocator_traits<Allocator>::template rebind_alloc <
std::pair<const key, Value >>>
{
private:
using allocator_type =
typename std::allocator_traits<Allocator>::template rebind_alloc <
std::pair<const key, Value >>;
using base_type =
std::map<key, Value, std::less<key>, allocator_type>; // NOLINT(modernize-use-transparent-functors)
public:
using base_type::base_type;
};
using json = nlohmann::json::with_object_t<object>;
} // namespace custom_object_key_test
+53
View File
@@ -28,6 +28,7 @@ using nlohmann::json;
#include "make_test_data_available.hpp"
#include "round_trip_corpus.hpp"
#include "test_utils.hpp"
#include "custom_object_key_type.hpp"
#include "sax_countdown.hpp"
using utils::SaxCountdown;
@@ -3357,3 +3358,55 @@ TEST_CASE("CBOR large strings and binaries (chunked reader)")
}
}
}
TEST_CASE("CBOR supports custom object key types")
{
using custom_json = custom_object_key_test::json;
using custom_key = custom_object_key_test::key;
custom_json::object_t object;
object.emplace(custom_key{"short"}, 1);
object.emplace(
custom_key{"a key longer than twenty-three characters"},
2);
const custom_json value(std::move(object));
const auto encoded = custom_json::to_cbor(value);
CHECK(nlohmann::json::from_cbor(encoded) == nlohmann::json
{
{"short", 1},
{"a key longer than twenty-three characters", 2}
});
}
TEST_CASE("CBOR supports custom object key types nested deeper than the recursion depth limit")
{
// below detail::recursion_depth_limit(), keys are written by
// write_cbor_iterative instead of write_cbor
using custom_json = custom_object_key_test::json;
using custom_key = custom_object_key_test::key;
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10;
custom_json value = 1;
nlohmann::json expected = 1;
for (std::size_t i = 0; i < depth; ++i)
{
// alternate short keys with ones long enough to need a length byte
const std::string name = (i % 2 == 0) ? "k" + std::to_string(i)
: "a key longer than thirty-one characters " + std::to_string(i);
custom_json::object_t object;
object.emplace(custom_key{name}, std::move(value));
value = custom_json(std::move(object));
nlohmann::json::object_t expected_object;
expected_object.emplace(name, std::move(expected));
expected = nlohmann::json(std::move(expected_object));
}
const auto encoded = custom_json::to_cbor(value);
CHECK(encoded == nlohmann::json::to_cbor(expected));
CHECK(nlohmann::json::from_cbor(encoded) == expected);
}
+501
View File
@@ -0,0 +1,501 @@
// __ _____ _____ _____
// __| | __| | | | JSON for Modern C++ (supporting code)
// | | |__ | | | | | | version 3.12.0
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
//
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
// SPDX-License-Identifier: MIT
#include "doctest_compatibility.h"
#include <nlohmann/json.hpp>
#include <cstddef>
#include <cstdint>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
// Object types with a user-defined key type. The key types differ in what they
// offer to the library: a conversion to std::string (implicit or explicit), a
// comparison with ==, a to_json overload, or a c_str() member.
namespace custom_key_test
{
class key_base
{
public:
key_base() = default;
key_base(const char* value)
: m_value(value)
{}
key_base(std::string value)
: m_value(std::move(value))
{}
// Required by JSON_DIAGNOSTICS, which reads object keys through data()
// when building the path of an exception.
const char* data() const noexcept
{
return m_value.data();
}
friend bool operator<(const key_base& lhs, const key_base& rhs)
{
return lhs.m_value < rhs.m_value;
}
protected:
std::string m_value;
};
// implicit conversion to std::string and operator==
class key_full : public key_base
{
public:
key_full() = default;
using key_base::key_base;
operator std::string() const
{
return m_value;
}
friend bool operator==(const key_full& lhs, const key_full& rhs)
{
return lhs.m_value == rhs.m_value;
}
};
// implicit conversion to std::string, but no operator==
class key_no_eq : public key_base
{
public:
key_no_eq() = default;
using key_base::key_base;
operator std::string() const
{
return m_value;
}
};
// explicit conversion to std::string, no operator==
class key_explicit : public key_base
{
public:
key_explicit() = default;
using key_base::key_base;
explicit operator std::string() const
{
return m_value;
}
};
// no conversion at all, only a to_json overload, no operator==
class key_to_json : public key_base
{
public:
key_to_json() = default;
using key_base::key_base;
const std::string& value() const
{
return m_value;
}
};
template<typename BasicJsonType>
void to_json(BasicJsonType& j, const key_to_json& k)
{
j = k.value();
}
// like key_to_json, but with size() and c_str()
class key_c_str : public key_base
{
public:
key_c_str() = default;
using key_base::key_base;
const std::string& value() const
{
return m_value;
}
std::size_t size() const
{
return m_value.size();
}
const char* c_str() const
{
return m_value.c_str();
}
};
template<typename BasicJsonType>
void to_json(BasicJsonType& j, const key_c_str& k)
{
j = k.value();
}
// std::map with key type K, ignoring the key type basic_json passes
template<class K>
struct object_for
{
template<class Key, class Value, class Compare, class Allocator>
using pair_allocator = typename std::allocator_traits<Allocator>::template rebind_alloc<std::pair<const K, Value>>;
template<class Key, class Value, class Compare, class Allocator>
using type = std::map<K, Value, std::less<K>, pair_allocator<Key, Value, Compare, Allocator>>; // NOLINT(modernize-use-transparent-functors)
};
using json_full = nlohmann::json::with_object_t<object_for<key_full>::type>;
using json_no_eq = nlohmann::json::with_object_t<object_for<key_no_eq>::type>;
using json_explicit = nlohmann::json::with_object_t<object_for<key_explicit>::type>;
using json_to_json = nlohmann::json::with_object_t<object_for<key_to_json>::type>;
using json_c_str = nlohmann::json::with_object_t<object_for<key_c_str>::type>;
// a key that is long enough to need a length byte in CBOR and MessagePack
const char* long_key_name(std::size_t i, std::string& storage);
const char* long_key_name(std::size_t i, std::string& storage)
{
storage = "a key longer than thirty-one characters " + std::to_string(i);
return storage.c_str();
}
// name of the key at nesting level i of a deep value
std::string deep_name(std::size_t i, bool long_keys);
std::string deep_name(std::size_t i, bool long_keys)
{
std::string storage;
return (long_keys && i % 2 == 1) ? std::string(long_key_name(i, storage)) : "k" + std::to_string(i);
}
// {"a": 1, "b": [true, null, "x"], "c": {"d": 2.5}, <keys of 23, 36, and 300 characters>}
// 23 is the longest CBOR length stored in the initial byte; 36 needs one
// length byte in CBOR and MessagePack, 300 needs two
template<class J>
J make_shallow()
{
using key_t = typename J::object_t::key_type;
J array = J::array();
array.push_back(J(true));
array.push_back(J(nullptr));
array.push_back(J("x"));
typename J::object_t inner;
inner.emplace(key_t("d"), J(2.5));
typename J::object_t object;
object.emplace(key_t("a"), J(1));
object.emplace(key_t("b"), std::move(array));
object.emplace(key_t("c"), J(std::move(inner)));
object.emplace(key_t(std::string(23, 'x')), J(2));
object.emplace(key_t(std::string(36, 'y')), J(3));
object.emplace(key_t(std::string(300, 'z')), J(4));
return J(std::move(object));
}
// {"k0": {"k1": {... {"k<depth-1>": 1} ...}}}
template<class J>
J make_deep(std::size_t depth, bool long_keys)
{
using key_t = typename J::object_t::key_type;
J value = 1;
for (std::size_t i = depth; i > 0; --i)
{
typename J::object_t object;
object.emplace(key_t(deep_name(i - 1, long_keys)), std::move(value));
value = J(std::move(object));
}
return value;
}
std::size_t deep_depth();
std::size_t deep_depth()
{
return nlohmann::detail::recursion_depth_limit() + 10;
}
// walk down the nesting levels without recursion and check the leaf
template<class J>
bool check_deep(const J& value, std::size_t depth, bool long_keys)
{
using key_t = typename J::object_t::key_type;
const J* current = &value;
for (std::size_t i = 0; i < depth; ++i)
{
if (!current->is_object() || current->size() != 1)
{
return false;
}
const auto it = current->find(key_t(deep_name(i, long_keys)));
if (it == current->end())
{
return false;
}
current = &it.value();
}
return current->is_number_integer() && current->template get<int>() == 1;
}
template<class J>
bool check_shallow(const J& value)
{
using key_t = typename J::object_t::key_type;
if (!value.is_object() || value.size() != 6)
{
return false;
}
const auto a = value.find(key_t("a"));
const auto b = value.find(key_t("b"));
const auto c = value.find(key_t("c"));
if (a == value.end() || b == value.end() || c == value.end())
{
return false;
}
const auto d = c->find(key_t("d"));
// basic_json::operator== needs operator== on the keys, which most of the
// key types do not have, so the values are checked through get<>()
return a->template get<int>() == 1
&& b->is_array() && b->size() == 3 && (*b)[0].template get<bool>() && (*b)[1].is_null()
&& (*b)[2].template get<std::string>() == "x"
&& d != c->end() && d->template get<double>() == 2.5
&& value.find(key_t(std::string(23, 'x')))->template get<int>() == 2
&& value.find(key_t(std::string(36, 'y')))->template get<int>() == 3
&& value.find(key_t(std::string(300, 'z')))->template get<int>() == 4;
}
template<class J>
bool is_missing(const J& value, const char* name)
{
return value.find(typename J::object_t::key_type(name)) == value.end();
}
// member access through find(): at() does not compile for key types without
// size() or a conversion to string_t (key_to_json), as in version 3.12.0
template<class J>
const J& member(const J& value, const char* name)
{
const auto it = value.find(typename J::object_t::key_type(name));
REQUIRE(it != value.end());
return *it;
}
} // namespace custom_key_test
TEST_CASE_TEMPLATE("custom object key types: copy", J,
custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit,
custom_key_test::json_to_json, custom_key_test::json_c_str)
{
SECTION("shallow")
{
const J original = custom_key_test::make_shallow<J>();
REQUIRE(custom_key_test::check_shallow(original));
const J copy(original); // NOLINT(performance-unnecessary-copy-initialization)
CHECK(custom_key_test::check_shallow(copy));
J assigned;
assigned = original;
CHECK(custom_key_test::check_shallow(assigned));
// the original is unchanged
CHECK(custom_key_test::check_shallow(original));
}
SECTION("deep")
{
const std::size_t depth = custom_key_test::deep_depth();
const J original = custom_key_test::make_deep<J>(depth, false);
REQUIRE(custom_key_test::check_deep(original, depth, false));
const J copy(original); // NOLINT(performance-unnecessary-copy-initialization)
CHECK(custom_key_test::check_deep(copy, depth, false));
J assigned;
assigned = original;
CHECK(custom_key_test::check_deep(assigned, depth, false));
CHECK(custom_key_test::check_deep(original, depth, false));
}
}
TEST_CASE_TEMPLATE("custom object key types: parse", J,
custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit,
custom_key_test::json_to_json, custom_key_test::json_c_str)
{
const J j = J::parse(R"({"a":1,"b":{"c":[1,2]}})");
CHECK(j.size() == 2);
CHECK(custom_key_test::member(j, "a").template get<int>() == 1);
CHECK(custom_key_test::member(custom_key_test::member(j, "b"), "c").size() == 2);
CHECK(custom_key_test::member(custom_key_test::member(j, "b"), "c")[1].template get<int>() == 2);
// a deeply nested document
const std::size_t depth = custom_key_test::deep_depth();
std::string text;
for (std::size_t i = 0; i < depth; ++i)
{
text += "{\"k" + std::to_string(i) + "\":";
}
text += "1";
text.append(depth, '}');
CHECK(custom_key_test::check_deep(J::parse(text), depth, false));
}
TEST_CASE_TEMPLATE("custom object key types: merge_patch, update, and insert", J,
custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit,
custom_key_test::json_to_json, custom_key_test::json_c_str)
{
SECTION("merge_patch")
{
J j = J::parse(R"({"a":1,"b":2,"n":{"x":1,"y":2}})");
j.merge_patch(J::parse(R"({"b":null,"c":3,"n":{"y":null,"z":3}})"));
CHECK(j.size() == 3);
CHECK(custom_key_test::member(j, "a").template get<int>() == 1);
CHECK(custom_key_test::is_missing(j, "b"));
CHECK(custom_key_test::member(j, "c").template get<int>() == 3);
CHECK(custom_key_test::member(j, "n").size() == 2);
CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "x").template get<int>() == 1);
CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "z").template get<int>() == 3);
}
SECTION("update")
{
J j = J::parse(R"({"a":1,"b":2,"n":{"x":1}})");
const J other = J::parse(R"({"b":3,"c":4,"n":{"y":2}})");
J replaced = j;
replaced.update(other);
CHECK(replaced.size() == 4);
CHECK(custom_key_test::member(replaced, "a").template get<int>() == 1);
CHECK(custom_key_test::member(replaced, "b").template get<int>() == 3);
CHECK(custom_key_test::member(replaced, "c").template get<int>() == 4);
CHECK(custom_key_test::member(replaced, "n").size() == 1);
CHECK(custom_key_test::member(custom_key_test::member(replaced, "n"), "y").template get<int>() == 2);
j.update(other, true);
CHECK(j.size() == 4);
CHECK(custom_key_test::member(j, "n").size() == 2);
CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "x").template get<int>() == 1);
CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "y").template get<int>() == 2);
}
SECTION("insert")
{
J j = J::parse(R"({"a":1,"b":2})");
const J other = J::parse(R"({"b":3,"c":4})");
j.insert(other.begin(), other.end());
CHECK(j.size() == 3);
CHECK(custom_key_test::member(j, "b").template get<int>() == 2);
CHECK(custom_key_test::member(j, "c").template get<int>() == 4);
}
}
TEST_CASE_TEMPLATE("custom object key types: at() reports a missing key", J,
custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit,
custom_key_test::json_c_str)
{
// not for key_to_json: at() needs the key's size() or a conversion to
// string_t for its error message, which also was the case in version 3.12.0
J j = J::parse(R"({"a":1})");
const J& j_const = j;
CHECK(j.at("a").template get<int>() == 1);
CHECK(j_const.at("a").template get<int>() == 1);
CHECK_THROWS_WITH_AS(j.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", typename J::out_of_range&);
CHECK_THROWS_WITH_AS(j_const.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", typename J::out_of_range&);
}
TEST_CASE_TEMPLATE("custom object key types: BSON", J,
custom_key_test::json_full, custom_key_test::json_no_eq)
{
SECTION("shallow")
{
const J value = custom_key_test::make_shallow<J>();
const nlohmann::json expected = custom_key_test::make_shallow<nlohmann::json>();
const std::vector<std::uint8_t> encoded = J::to_bson(value);
CHECK(encoded == nlohmann::json::to_bson(expected));
CHECK(nlohmann::json::from_bson(encoded) == expected);
}
SECTION("deep")
{
const std::size_t depth = custom_key_test::deep_depth();
const J value = custom_key_test::make_deep<J>(depth, false);
const nlohmann::json expected = custom_key_test::make_deep<nlohmann::json>(depth, false);
const std::vector<std::uint8_t> encoded = J::to_bson(value);
CHECK(encoded == nlohmann::json::to_bson(expected));
CHECK(nlohmann::json::from_bson(encoded) == expected);
}
}
TEST_CASE_TEMPLATE("custom object key types: CBOR", J,
custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit,
custom_key_test::json_to_json, custom_key_test::json_c_str)
{
SECTION("shallow")
{
const J value = custom_key_test::make_shallow<J>();
const nlohmann::json expected = custom_key_test::make_shallow<nlohmann::json>();
const std::vector<std::uint8_t> encoded = J::to_cbor(value);
CHECK(encoded == nlohmann::json::to_cbor(expected));
CHECK(nlohmann::json::from_cbor(encoded) == expected);
}
SECTION("deeper than the recursion depth limit")
{
const std::size_t depth = custom_key_test::deep_depth();
const J value = custom_key_test::make_deep<J>(depth, true);
const nlohmann::json expected = custom_key_test::make_deep<nlohmann::json>(depth, true);
const std::vector<std::uint8_t> encoded = J::to_cbor(value);
CHECK(encoded == nlohmann::json::to_cbor(expected));
CHECK(nlohmann::json::from_cbor(encoded) == expected);
}
}
TEST_CASE_TEMPLATE("custom object key types: MessagePack", J,
custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit,
custom_key_test::json_to_json, custom_key_test::json_c_str)
{
SECTION("shallow")
{
const J value = custom_key_test::make_shallow<J>();
const nlohmann::json expected = custom_key_test::make_shallow<nlohmann::json>();
const std::vector<std::uint8_t> encoded = J::to_msgpack(value);
CHECK(encoded == nlohmann::json::to_msgpack(expected));
CHECK(nlohmann::json::from_msgpack(encoded) == expected);
}
SECTION("deeper than the recursion depth limit")
{
const std::size_t depth = custom_key_test::deep_depth();
const J value = custom_key_test::make_deep<J>(depth, true);
const nlohmann::json expected = custom_key_test::make_deep<nlohmann::json>(depth, true);
const std::vector<std::uint8_t> encoded = J::to_msgpack(value);
CHECK(encoded == nlohmann::json::to_msgpack(expected));
CHECK(nlohmann::json::from_msgpack(encoded) == expected);
}
}
+53
View File
@@ -31,6 +31,7 @@ using nlohmann::json;
#include "make_test_data_available.hpp"
#include "round_trip_corpus.hpp"
#include "test_utils.hpp"
#include "custom_object_key_type.hpp"
#include "sax_countdown.hpp"
using utils::SaxCountdown;
@@ -2522,3 +2523,55 @@ TEST_CASE("MessagePack large strings and binaries (chunked reader)")
}
}
}
TEST_CASE("MessagePack supports custom object key types")
{
using custom_json = custom_object_key_test::json;
using custom_key = custom_object_key_test::key;
custom_json::object_t object;
object.emplace(custom_key{"short"}, 1);
object.emplace(
custom_key{"a key longer than thirty-one characters"},
2);
const custom_json value(std::move(object));
const auto encoded = custom_json::to_msgpack(value);
CHECK(nlohmann::json::from_msgpack(encoded) == nlohmann::json
{
{"short", 1},
{"a key longer than thirty-one characters", 2}
});
}
TEST_CASE("MessagePack supports custom object key types nested deeper than the recursion depth limit")
{
// below detail::recursion_depth_limit(), keys are written by
// write_msgpack_iterative instead of write_msgpack
using custom_json = custom_object_key_test::json;
using custom_key = custom_object_key_test::key;
const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10;
custom_json value = 1;
nlohmann::json expected = 1;
for (std::size_t i = 0; i < depth; ++i)
{
// alternate short keys with ones long enough to need a length byte
const std::string name = (i % 2 == 0) ? "k" + std::to_string(i)
: "a key longer than thirty-one characters " + std::to_string(i);
custom_json::object_t object;
object.emplace(custom_key{name}, std::move(value));
value = custom_json(std::move(object));
nlohmann::json::object_t expected_object;
expected_object.emplace(name, std::move(expected));
expected = nlohmann::json(std::move(expected_object));
}
const auto encoded = custom_json::to_msgpack(value);
CHECK(encoded == nlohmann::json::to_msgpack(expected));
CHECK(nlohmann::json::from_msgpack(encoded) == expected);
}