nlohmann / nlohmann/json

to_cbor()/to_msgpack() allocate a temporary basic_json for every object key

Open
#5,318 2 comments 0 reactions 1 assignee View on GitHub

@alexprabhat99 is already working on this.

Since Jul 28, 2026.

aspect: binary formats good first issue kind: enhancement/improvement
Dominant language
C++
Stars
50.6k
Forks
7.5k
Avg merge
4d 17h
Merged PRs (30d)
58

Description

Description

write_cbor() serializes object keys by calling itself on the key:

https://github.com/nlohmann/json/blob/8ec98e2c9ea9c0adc15a791feb3d4d4468621743/include/nlohmann/detail/output/binary_writer.hpp#L403-L407

el.first is a string_t, but the parameter is const BasicJsonType&, so each key is implicitly converted to a temporary basic_json — which heap-allocates a string_t (plus its buffer for keys beyond SSO) — purely to reach the value_t::string case a few lines above. write_msgpack() has the identical pattern.

The UBJSON and BSON writers in the same file already do it the direct way, writing the key's characters straight to the output adapter without constructing a basic_json.

Measurement

Counting global operator new calls while serializing a 1000-entry object (-O2, g++ 13.3.0):

case allocations
to_cbor, array of 1000 ints (baseline) 13
to_cbor, object, 1000 short keys (SSO) 1016
to_cbor, object, 1000 64-char keys 2016
to_msgpack, object, 1000 short keys 1016
to_ubjson, object, 1000 short keys 17
to_bson, object, 1000 short keys 14

So CBOR and MessagePack pay one allocation per key even when the key fits in the small-string buffer (the string_t object itself is heap-allocated by basic_json), and two per key otherwise. UBJSON and BSON pay none.

Minimal code example
static long allocs = 0;
void* operator new(std::size_t n) { ++allocs; void* p = std::malloc(n); if (!p) throw std::bad_alloc(); return p; }
void operator delete(void* p) noexcept { std::free(p); }
void operator delete(void* p, std::size_t) noexcept { std::free(p); }

#include <nlohmann/json.hpp>
using json = nlohmann::json;

int main()
{
    json j = json::object();
    for (int i = 0; i < 1000; ++i) { char k[8]; std::snprintf(k, sizeof k, "k%04d", i); j[k] = i; }

    allocs = 0; (void)json::to_cbor(j);   std::printf("cbor   %ld\n", allocs); // 1016
    allocs = 0; (void)json::to_ubjson(j); std::printf("ubjson %ld\n", allocs); // 17
}
Suggested fix

Factor the string-writing body of the value_t::string case out of write_cbor into a helper taking const string_t& (say write_cbor_string), call it from both the value_t::string case and the key loop, and do the same for write_msgpack. This is the same shape as the BSON writer cleanup in #5313, which changed write_bson_unsigned to take the underlying value rather than the basic_json.

Compiler and operating system

g++ 13.3.0, Ubuntu 24.04, -std=c++14, -O2

Library version

develop @ 8ec98e2

Would the fix be breaking?

No. This is a purely internal refactor of two private member functions of detail::binary_writer:

  • Byte-for-byte identical output. The extracted helper would run the same length-prefix and character-writing code the value_t::string case runs today, so to_cbor/to_msgpack produce exactly the same bytes. Existing round-trip tests should pass unchanged.
  • No API/ABI surface. binary_writer is in detail and the affected functions are not part of the documented API. No signature in basic_json changes.
  • One thing to watch: object keys are object_t::key_type, which is not necessarily string_t for a custom ObjectType. The helper should be written so key types that merely convert to string_t still compile — either by templating it or by keeping write_cbor(el.first) as a fallback for non-string_t key types. Getting this wrong would break exotic basic_json specializations at compile time, so it deserves a test with a custom object type.

I'm happy to open a PR for this if it's wanted.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.