Skip to content

JSON_RECURSION_DEPTH_LIMIT

#define JSON_RECURSION_DEPTH_LIMIT /* value */

This macro sets how many nesting levels the operations that walk a value recurse into on the call stack. Beyond that, they continue with an explicit stack on the heap. The value is an integer constant from 0 to 254; the default is 128.

The operations are copying a value, comparing two values, serializing with dump or operator<<, hashing with std::hash, merge_patch, update with merge_objects, diff, merge_diff, converting a value to another specialization of basic_json, destroying a value, and the binary writers to_cbor, to_msgpack, to_ubjson, to_bjdata, and to_bon8. They share this one limit; none has a limit of its own. Recursing is the fastest way to walk a value, but a value nested deeply enough would exhaust the call stack, so the library recurses only as far as the limit and finishes the levels below with an explicit stack. This is what makes all of them safe for untrusted input (see Nesting depth). Parsing, the binary readers, flatten, unflatten, and to_bson always use an explicit stack and do not depend on the macro. The lockstep comparison of two json_view documents has a fixed bound of its own, 48 levels, and does not depend on it either.

The call stack that these operations need grows with the limit: roughly a few kilobytes plus a cost per level times the limit, where the cost per level is up to about 2.3 KiB in an unoptimized and up to about 0.5 KiB in an optimized build (see Stack needed). The default of 128 levels is tuned for threads with ordinary stacks of a few hundred kilobytes or more. Set a lower limit if your threads have small stacks (RTOS tasks, fibers, threads of the musl C library, which has a default stack of 128 KiB), or a higher limit to trade stack for speed on deeply nested values.

The macro changes only how much stack is used and how fast the operations are. It never changes a result: every operation returns the same value for every setting of the limit, for values nested less deeply than the limit as well as for values nested more deeply.

Default definition

The default value is 128.

#define JSON_RECURSION_DEPTH_LIMIT 128

When the macro is not defined, the library defines it to its default value.

Notes

Allowed values

The value must be an integer constant expression from 0 to 254; any other value (a negative number, a number above 254, a floating-point number such as 254.0, or true or false) is rejected at compile time with an error message that names the macro. A character of any character type ('a', L'a', u8'a', ...) is rejected, too, although it is an integer.

  • 0 is allowed, and means that every operation uses its explicit stack from the first level on and no operation recurses over the nesting of a value at all. That needs a constant amount of call stack (at most 16 KiB in the measurements below, for a value nested 20,000 levels deep) and is the setting for tiny stacks, at the price of speed (see Speed).
  • 254 is the largest allowed value. Copying a value and comparing two values (with <=>, or with the operators on objects and arrays of a custom type) cannot pass the depth down as an argument, because the signatures of the copy constructor and the operators are fixed. They count the levels they have descended into in a variable of type std::uint8_t that exists once per thread (see JSON_NO_THREAD_LOCAL). Only the levels that are descended into are counted, so the count never exceeds the limit, not even when user code (a copy constructor of a custom base class, say) copies a value while a copy is going on. A byte holds 255 at most, and the limit stays below that, so that the count cannot wrap around. (More levels would hardly be useful: 254 levels need up to 576 KiB of stack in an unoptimized build already, twice as much as the default.)

Stack needed

The tables show the smallest stack that an operation needs, in KiB, at several limits, on a value that is nested more deeply than the limit (limit + 10 levels). Each cell shows the result for Clang (Apple clang 21) and for GCC 16, separated by a slash; both measured on macOS with arm64, compiling in C++20 mode. The operations run in a thread whose stack has the size given, and are not given any stack that they do not need: the value is built, and the result destroyed, outside of that thread. Stacks can only be set in steps of 16 KiB (the page size), so 16 means "16 KiB or less". A row stands for all operations listed; the table shows the largest value among them.

Unoptimized (-O0):

Operation limit 16 limit 32 limit 64 limit 128 limit 254
dump() and dump(indent), operator<< 48 / 32 80 / 64 144 / 128 288 / 256 576 / 512
copy construction and assignment 32 / 16 48 / 32 112 / 80 224 / 144 432 / 304
== 16 / 16 16 / 16 16 / 16 48 / 32 96 / 48
<=> (C++20) 16 / 16 16 / 16 32 / 32 64 / 64 144 / 128
std::hash 16 / 16 16 / 16 32 / 32 80 / 64 160 / 128
merge_patch, update(j, true), patch 32 / 16 48 / 32 112 / 80 224 / 144 432 / 304
diff 16 / 16 16 / 16 32 / 32 64 / 48 144 / 96
merge_diff 16 / 16 48 / 32 80 / 48 176 / 112 352 / 224
to_cbor, to_msgpack 16 / 16 32 / 16 64 / 32 112 / 80 224 / 160
to_ubjson, to_bjdata 16 / 16 16 / 16 48 / 32 96 / 64 176 / 112
to_bon8 16 / 16 16 / 16 16 / 16 32 / 16 64 / 48
conversion to another basic_json type 32 / 16 64 / 32 112 / 80 224 / 144 448 / 304
destruction 16 / 16 16 / 16 16 / 16 16 / 16 32 / 32

Optimized (-O2):

Operation limit 16 limit 32 limit 64 limit 128 limit 254
dump() and dump(indent), operator<< 16 / 16 16 / 16 16 / 16 16 / 16 32 / 32
copy construction and assignment 16 / 16 16 / 16 16 / 16 32 / 32 64 / 48
== 16 / 16 16 / 16 16 / 16 16 / 16 16 / 16
<=> (C++20) 16 / 16 16 / 16 16 / 16 16 / 16 48 / 16
std::hash 16 / 16 16 / 16 16 / 16 32 / 48 80 / 80
merge_patch, update(j, true), patch 16 / 16 16 / 16 16 / 16 32 / 32 64 / 48
diff 16 / 16 16 / 16 16 / 32 48 / 48 96 / 112
merge_diff 16 / 16 16 / 16 16 / 16 48 / 32 96 / 64
to_cbor, to_msgpack 16 / 16 16 / 16 16 / 16 16 / 16 16 / 16
to_ubjson, to_bjdata 16 / 16 16 / 16 16 / 16 32 / 32 64 / 64
to_bon8 16 / 16 16 / 16 16 / 16 16 / 32 32 / 64
conversion to another basic_json type 16 / 16 16 / 16 16 / 16 32 / 48 64 / 96
destruction 16 / 16 16 / 16 16 / 16 16 / 16 16 / 16

With a limit of 0, every operation in the tables ran in a 16 KiB stack at -O0 (Clang and GCC), on a value nested 20,000 levels deep. The operations that do not depend on the macro (flatten, unflatten, to_bson) needed 16 KiB or less at every limit.

The rule of thumb is

stack needed ≈ a few KiB + cost per level × limit

with a cost per level of up to 2.3 KiB (dump at -O0 with Clang; 2.1 KiB with GCC), 1.7 KiB for copying and merging (1.3 KiB with GCC), and 0.1 to 0.5 KiB for most other operations at -O0; and 0.5 KiB or less at -O2. The numbers come from the difference between the limits 128 and 254 in the tables, so they are rough: other compilers, other targets (the frames of x86-64 code are about as large), other standard libraries, JSON_DIAGNOSTICS, and sanitizers change them. Measure with your own tool chain (see the example below) rather than relying on the tables. The stack that your own code and the operating system need comes on top of these numbers.

Which limit to choose, for the stack that you can spare for the library (the largest limit of those measured for which the slowest operation needs at most that much):

Stack for the library optimized (-O2) unoptimized (-O0)
32 KiB 64 0
64 KiB 128 16
128 KiB (the default stack of threads on musl) 254 32
256 KiB 254 64
512 KiB 254 128
1 MiB or more 254 254

Speed

Operations are faster the more of a value they handle with plain recursion. The levels below the limit are handled with an explicit stack, which has to be kept on the heap and costs more per level. The macro therefore trades speed against stack.

The table shows the time of copying a value (including destroying the copy), of == on two equal values, of dump, and of std::hash for several limits, relative to the default of 128 (1.00 means the same time; more is slower). It was measured with Clang 21 at -O2 in C++17 mode on macOS with arm64, as the best of seven runs of each setting (two rounds, best of both). The documents are ordinary JSON files that are nested 7 (canada.json), 8 (citm_catalog.json), 10 (twitter.json), and 2 levels deep (jeopardy.json); the other values are one array or object with one member per level, nested 1,000 and 100,000 levels deep. The absolute time at the limit 128 is given in the third column. Differences of up to about 0.1 are noise: a different limit changes the layout of the compiled code, and timings of the same binary vary by about as much. The slow columns (limits 0 and 1) vary more: repeating std::hash on canada.json gave 1.27 to 1.30 for limit 0 and 1.4 to 1.9 for limit 1, so read the cells of those two columns as accurate to about 0.3.

Value Operation limit 128 (absolute) limit 0 limit 1 limit 16 limit 254
canada.json (2.1 MB) copy 3.5 ms 1.80 1.80 0.99 1.00
canada.json (2.1 MB) == 350.5 µs 2.36 2.38 1.00 0.99
canada.json (2.1 MB) dump 2.4 ms 1.11 1.11 1.00 1.00
canada.json (2.1 MB) std::hash 920.2 µs 1.33 1.59 1.00 1.00
citm_catalog.json (1.7 MB) copy 1.8 ms 1.38 1.36 1.00 0.99
citm_catalog.json (1.7 MB) == 275.5 µs 1.31 1.31 0.99 1.00
citm_catalog.json (1.7 MB) dump 314.6 µs 1.24 1.23 1.00 1.00
citm_catalog.json (1.7 MB) std::hash 309.1 µs 1.77 1.42 1.06 1.05
twitter.json (0.6 MB) copy 865.7 µs 1.45 1.47 1.01 1.00
twitter.json (0.6 MB) == 135.8 µs 1.26 1.23 1.00 1.00
twitter.json (0.6 MB) dump 224.6 µs 1.12 1.12 1.01 1.00
twitter.json (0.6 MB) std::hash 132.9 µs 1.28 1.27 1.03 1.04
jeopardy.json (53 MB) copy 111.1 ms 1.22 1.25 1.00 1.00
jeopardy.json (53 MB) == 23.5 ms 1.29 1.53 1.07 1.00
jeopardy.json (53 MB) dump 33.2 ms 1.14 1.34 1.03 1.01
jeopardy.json (53 MB) std::hash 19.9 ms 1.17 1.52 1.05 1.00
arrays, 1,000 levels copy 69.9 µs 0.95 0.98 0.92 0.99
arrays, 1,000 levels == 10.4 µs 1.02 1.01 0.96 1.00
arrays, 1,000 levels dump 9.6 µs 0.97 0.96 0.97 1.06
arrays, 1,000 levels std::hash 16.9 µs 1.00 1.01 1.01 0.98
objects, 1,000 levels copy 77.1 µs 0.98 0.97 0.97 1.03
objects, 1,000 levels == 12.0 µs 0.96 0.95 0.95 1.07
objects, 1,000 levels dump 12.6 µs 0.97 0.95 0.98 1.01
objects, 1,000 levels std::hash 17.3 µs 0.99 1.01 0.98 1.00
arrays, 100,000 levels copy 6.4 ms 1.00 1.00 1.00 1.00
arrays, 100,000 levels == 1.1 ms 1.01 1.03 1.00 1.06
arrays, 100,000 levels dump 917.9 µs 1.01 1.01 1.02 1.01
arrays, 100,000 levels std::hash 1.7 ms 1.04 1.05 0.99 0.99
objects, 100,000 levels copy 7.5 ms 0.99 1.02 1.01 0.99
objects, 100,000 levels == 1.3 ms 1.02 1.09 1.08 0.96
objects, 100,000 levels dump 1.2 ms 1.06 0.99 1.01 0.99
objects, 100,000 levels std::hash 1.8 ms 1.00 1.00 1.00 1.00

The results are as follows.

  • A limit of 16 or more does not change the speed of ordinary documents (differences are within noise), because they are not nested more deeply than that anyway. A document nested more deeply than the limit is walked partly with the explicit stack.
  • A limit of 0 or 1 makes the operations on ordinary documents 1.1 to 2.4 times as slow, because even their first levels are handled with the explicit stack. The slowdown is largest for == on a document of many small containers (canada.json: 2.4 times) and for copying (up to 1.8 times).
  • For values that are nested far more deeply than any limit (1,000 and 100,000 levels deep), the limit makes no measurable difference: almost all of the levels are handled with the explicit stack for every setting. Raising the limit gains nothing for them, and lowering it costs nothing.

Other macros

  • With JSON_NO_THREAD_LOCAL defined (or for Clang targeting MinGW, where the library defines it by itself), copying and comparing never descend, so for them the limit does not matter. The other operations still recurse as many levels as the limit says. The range of the limit is the same.
  • The macro is not part of the ABI tag, as JSON_NO_THREAD_LOCAL is not, and it is not undefined at the end of json.hpp.

Use the same value in every translation unit

All translation units of a program that include the library must define the same value. Otherwise, the program violates the One Definition Rule: the function nlohmann::detail::recursion_depth_limit() and all the functions that use it are defined differently in different translation units. Which limit a call uses then depends on which functions the compiler inlined (an inlined copy uses the value of its own translation unit) and which out-of-line copy of each remaining function the linker kept, which is not specified. A function compiled with one value can call a function compiled with another, so a single operation can mix both limits.

Mixing values cannot corrupt the byte per thread that counts the levels of copying and comparing: each limit is at most 254, and all of them stop counting at their own limit, so the count never exceeds 254, whichever translation unit's definition runs. The counter exists once per thread and basic_json specialization, and is shared by all translation units. Nor can mixing values change any result, as no result depends on the limit. What changes is how much stack is used and how fast the operations are. Nevertheless, avoid it: the program has undefined behavior as far as the C++ standard is concerned.

Examples

Example: lower the limit

The example nests an array 5,000 levels deep. With the limit set to 16, the library recurses over the first 16 levels and over the rest uses an explicit stack. The results are those of the default limit.

#include <functional>
#include <iostream>
#include <utility>

// walk over at most 16 nesting levels on the call stack, and over the rest with an explicit stack
#define JSON_RECURSION_DEPTH_LIMIT 16
#include <nlohmann/json.hpp>

using json = nlohmann::json;

int main()
{
    // an array nested 5,000 levels deep, built without recursion
    json j = 1;
    for (int i = 0; i < 5000; ++i)
    {
        json outer = json::array();
        outer.push_back(std::move(j));
        j = std::move(outer);
    }

    // the results are those of any other limit
    const json copy = j;
    std::cout << std::boolalpha;
    std::cout << "limit: " << JSON_RECURSION_DEPTH_LIMIT << '\n';
    std::cout << "copy equals original: " << (copy == j) << '\n';
    std::cout << "length of dump(): " << j.dump().size() << '\n';
    std::cout << "same hash: " << (std::hash<json> {}(copy) == std::hash<json> {}(j)) << '\n';
}

Output:

limit: 16
copy equals original: true
length of dump(): 10001
same hash: true
Example: find out how much stack an operation needs

The program runs dump() on a value nested depth levels deep in a thread with a stack of kib KiB. Compile it with the options and the limit that you use, e.g. -DJSON_RECURSION_DEPTH_LIMIT=64, run it with depth set to the limit plus 10 and decreasing values of kib (the stack size must be a multiple of the page size, often 4 or 16 KiB), and note the smallest stack with which it still finishes. That is how the tables above were measured.

#include <cstdlib>
#include <iostream>
#include <string>
#include <utility>
#include <pthread.h>
#include <nlohmann/json.hpp>

static nlohmann::json value;

static void* run(void*)
{
    std::cout << "dump() ok, " << value.dump().size() << " bytes\n";
    return nullptr;
}

int main(int argc, char** argv)
{
    const int depth = std::atoi(argv[1]);
    const std::size_t kib = static_cast<std::size_t>(std::atoi(argv[2]));

    // built on the main thread, without recursion
    value = 1;
    for (int i = 0; i < depth; ++i)
    {
        nlohmann::json outer = nlohmann::json::array();
        outer.push_back(std::move(value));
        value = std::move(outer);
    }

    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setstacksize(&attr, kib * 1024);
    pthread_t thread;
    pthread_create(&thread, &attr, run, nullptr);
    pthread_join(thread, nullptr);
}

See also

Version history

  • Added in version 3.13.0 unreleased.