Skip to content

Restore v3.12.0 support for custom object key types - #5795

Merged
nlohmann merged 9 commits into
developfrom
fix/custom-key-type-compat
Oct 11, 2026
Merged

nlohmann merged 9 commits into
developfrom
fix/custom-key-type-compat

Conversation

@nlohmann

@nlohmann nlohmann commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Custom object_t types whose key_type is not string_t (for example a std::map keyed by a user-defined class) compiled with v3.12.0 for several APIs that unreleased changes on develop broke. This PR makes everything that compiled with v3.12.0 compile again, and pins it with a test.

Found while reviewing #5328, which documented a new requirement that object_t::key_type be implicitly convertible to string_t. A compile sweep (28 APIs × 5 key types × C++11/C++17, v3.12.0 vs develop) found these regressions:

API Broken by Required since Fix
to_bson #5553 key_type is string_t (all custom keys, even well-behaved ones) write the nested entry's header where the entry is found instead of keeping a const string_t* to the key
copy of deep values; parse, merge_patch, update, insert #5389 operator== on keys (std::map only needs operator<) copy_keys_equal() uses == if available, else pairs values via find()
to_cbor, to_msgpack #5746, #5781, #5328 implicit conversion to string_t write_cbor_key() / write_msgpack_key(): fast path for convertible keys (as in #5328), otherwise a temporary basic_json as in v3.12.0; the static_asserts are removed
at() (key not found) #5727 string_t constructible from the key key_for_message() passes non-convertible keys to concat() unchanged, as v3.12.0 did

The sentence #5328 added to object_t.md is reverted. After this PR, the sweep shows no regressions for any key type or standard.

The key types in the new tests/src/unit-custom-object-key-type.cpp:

  • implicit operator std::string and operator==
  • implicit operator std::string, no operator==
  • only explicit operator std::string
  • no conversion, only a to_json overload
  • the same with size()/c_str()

It tests copy (shallow and deeper than recursion_depth_limit()), parse, merge_patch/update/insert, at(), BSON (the two key types BSON supported in v3.12.0), and byte-identical CBOR/MessagePack output, shallow and deep. The deep cases cover the *_iterative writers.

Verified locally: the new test in C++11, C++20, and with JSON_DIAGNOSTICS, with the CI's -Weverything flags; unit-bson, unit-cbor, unit-msgpack, unit-custom-object-type, unit-element_access2, unit-constructor1 in C++11; make amalgamate clean.

Breaking changes

None. This removes requirements on object_t::key_type that unreleased changes added after v3.12.0, including the compatibility note in #5328. Nothing changes for string_t keys: they still take the direct paths, and CBOR/MessagePack still writes them without a temporary basic_json.

This PR was written by Claude Code on behalf of @nlohmann.

🤖 Generated with Claude Code

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>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
{
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In line with your recent deduplication changes, is two times enough duplication to make a helper function?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, done in b9c46c5: write_bson_nested_head() writes the type, key and size of a nested element. It has to be called inside the loops (not once after them, as before) because an object key may only convert to a temporary string_t, so it can't be kept by pointer past the loop.

(Written by Claude Code.)

GCC with -Werror=float-equal rejects comparing the double value with ==.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
develop (#5798) added tests/src/unit-custom-object-key-type.cpp with the
custom object key tests moved out of unit-cbor.cpp and unit-msgpack.cpp.
This branch added a file of the same name; keep develop's file and move
this branch's tests:

- the key types, helpers, and test bodies go to custom_key_test.hpp;
- unit-custom-key-type-{full,no-eq,explicit,to-json,c-str}.cpp run the
  tests for one object type each, as one file with all five
  specializations failed to link with MinGW ("relocation truncated to
  fit", more than 65535 sections, see .github/workflows/windows.yml);
- the at() test expects the "(bytes 0-7)" prefix with
  JSON_DIAGNOSTIC_POSITIONS.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Comment thread include/nlohmann/json.hpp Outdated
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this copy a string unnecessarily if it has both size() and data() but can also be used to construct string_t? Is that not even a consideration because this is in the exception path? What type even can be used as a key but needs a string_t wrapper?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — yes, it did. A miss on a string_t (or string_view) key copied the key only so concat() could copy it again; v3.12.0 passed the key_type overload's key through directly. Fixed in e6aba4b: keys with data() and size() now go to concat() unchanged, and only other keys are converted to string_t. It's only on the throwing path, so the cost was small, but there was no reason for it.

The conversion is still needed for key types that convert to string_t but have no size(). Examples are key_full, key_no_eq and key_explicit in the new tests: they only have data(), for JSON_DIAGNOSTICS, so concat() can't take them directly. String literals are converted too, as in v3.12.0.

(Written by Claude Code.)

The reference is only passed to concat() within the full-expression that
holds the key, like the similar helpers in binary_writer.hpp.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
clang-tidy (cppcoreguidelines-non-private-member-variables-in-classes)
rejects the protected member; the derived key types use a protected
accessor instead.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
key_for_message() converted every key that string_t can be constructed
from, so a miss on a string_t or string_view key copied it before
concat() copied it again. Keys that concat() can append through data()
and size() are now passed through; only other keys (string literals,
key types that just convert to string_t) are converted.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann nlohmann added this to the Release 3.13.0 milestone Oct 10, 2026
@nlohmann nlohmann added the 🚀 ready to merge Ready to merge - just waiting for CI to complete. label Oct 10, 2026
Signed-off-by: Niels Lohmann <mail@nlohmann.me>

# Conflicts:
#	include/nlohmann/json.hpp
#	single_include/nlohmann/json.hpp
@nlohmann
nlohmann merged commit eb8899a into develop Oct 11, 2026
3 of 167 checks passed
@nlohmann
nlohmann deleted the fix/custom-key-type-compat branch October 11, 2026 06:24
nlohmann added a commit that referenced this pull request Oct 11, 2026
#5795 and #5806 changed binary_writer.hpp but were merged without
updated amalgamated files, so the check_amalgamation run on develop
(c59ad95) fails. This is the output of 'make amalgamate'; no source
files change.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann added a commit that referenced this pull request Oct 11, 2026
dump() instantiates the serializer for the whole value type, which does
not compile for object key types it cannot write (e.g., the
custom_key_test::key_c_str of #5795), even though the value is always a
number here. Produce the decimal digits directly instead; they are
generated per digit so that the minimum of a signed type is never
negated as a whole.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

aspect: binary formats BSON, CBOR, MessagePack, UBJSON documentation L 🚀 ready to merge Ready to merge - just waiting for CI to complete. tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants