Commit Graph
5171 Commits
Author SHA1 Message Date
Niels Lohmann dc194c94d9 Stop excluding the Unicode tests in CI
The compiler matrix, icpc/icpx/nvhpc, the default-compiler job, the
Windows MinGW and clang-cl jobs, and AppVeyor Debug builds excluded
test-unicode because it took several minutes. It now takes ~20 s, so
run it everywhere except under Valgrind.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 22:07:47 +02:00
Niels Lohmann 9ffd033af7 Merge the Unicode tests into unit-unicode.cpp
The Unicode tests were split into five files (#2889) so they could run
in parallel. After #5418 they take ~20 s together (unoptimized), so one
file is enough. This removes four copies of the check helpers and the
progress output, whose hard-coded totals were stale, and drops
doctest::skip() so JSON_FastTests configurations run them too. The
raised timeout for test-unicode4 is no longer needed.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 22:07:46 +02:00
Niels Lohmann d304501fa7 Cover every byte class in the ill-formed UTF-8 sweeps
Pinning the bytes around the invalid one to 0x80 lost coverage: with
error_handler_t::ignore/replace, the serializer re-reads the invalid
byte and decodes the following bytes, and its decoder distinguishes
the continuation classes 0x80-0x8F, 0x90-0x9F, and 0xA0-0xBF, as do
the lexer's range checks.

Iterate those positions over the first and last byte of each class
within the valid range instead (utils::utf8_continuation_bytes). The
invalid byte still takes all 256 values. Defining
JSON_TEST_UTF8_EXHAUSTIVE restores the full Cartesian product, with
the same assertion counts as before #5418.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 22:00:35 +02:00
Niels Lohmann 21c8935269 Check the JSON Pointer roundtrip for every code point again
Sampling every 64th element of all_unicode.json gave up the claim that
every code point is tested for a small saving; the stringstream
removal keeps most of the speedup.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 22:00:34 +02:00
Niels Lohmann 96cedfec81 Compile unit-msgpack.cpp only once
unit-msgpack.cpp mentioned JSON_HAS_CPP_17 for a single std::byte test
case, so the whole file was also compiled and run as test-msgpack_cpp17.
Move that test case into unit-msgpack-cpp17.cpp, as was done for
unit-items-cpp17.cpp.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 21:48:35 +02:00
Niels Lohmann d27698ee55 Run the cheap binary format size tests unconditionally
Only jeopardy.json (52 MB, ~20 s unoptimized) needs doctest::skip();
move it into its own test case so canada.json, twitter.json,
citm_catalog.json and sample.json (~1.5 s) also run in JSON_FastTests
configurations.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 21:47:17 +02:00
Niels Lohmann 295ff0f778 Speed up unit-unicode1
Format \uxxxx escapes by hand instead of constructing a stringstream
for each of the ~1.1M code points, and check the JSON Pointer
escape/unescape roundtrip on every 64th element of all_unicode.json
plus '~' and '/', the only characters escaping treats specially.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 21:46:39 +02:00
Niels Lohmann e802c98da8 Pin unrelated bytes in the remaining ill-formed UTF-8 sweeps
The wrong-4th-byte sections of unit-unicode3/4/5 became live with #5499
and swept every valid 2nd/3rd byte again (2.4M iterations in unicode4
alone). Pin those bytes like the 2nd/3rd-byte sections, and do the
same for the 3-byte sequences in unit-unicode2.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 21:46:08 +02:00
elix3r c6bb0844f3 Cut Unicode ill-formed byte sweeps to one representative prefix
Wrong-2nd and wrong-3rd-byte sections iterated every valid trailing
byte, which dominated Linux --no-skip runtime without testing extra
properties. Pin those bytes to a single valid continuation.

The fourth-byte typo is left to #5416 so this change stands alone.

Signed-off-by: elix3r <157088510+22elix3r@users.noreply.github.com>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 21:44:47 +02:00
Niels Lohmann 373005f7ac Fix MSVC: avoid reserving by faked size in MessagePack size tests (#5604)
The "Size above uint32" tests for arrays and objects fake a container
size of 2^32 and expect to_msgpack() to throw out_of_range.412. But
to_msgpack(j) first reserves binary_reserve_hint(j) bytes, which is
size + 1 for arrays and 2 * size + 1 for objects, i.e. 4 or 8 GiB.
Linux and macOS overcommit, so the reservation succeeds; on Windows it
throws std::bad_alloc before the size check is reached (seen with
msvc-vs2026 Debug x64 on the object test).

Write into a caller-owned vector instead, so nothing is reserved.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 20:57:16 +02:00
Niels Lohmann de6acd651e Fix CI: wrap an overlong line in the cbor_tag_handler_t documentation (#5602)
#5559 added a 224-character line to cbor_tag_handler_t.md; the
documentation style check allows at most 160.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 20:40:33 +02:00
Niels Lohmann 56ddcb65f0 Fix documentation style check: wrap long line in cbor_tag_handler_t.md (#5603)
The line added in #5559 exceeded the 160-character limit enforced by
docs/mkdocs/scripts/check_structure.py, breaking the documentation build.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 20:40:13 +02:00
Niels Lohmann 509c07041f Fix CI: clang-tidy and clang/libstdc++ 10 in the MessagePack size tests (#5599)
The tests added by #5515 fail two ways on develop:

- clang-tidy reports the size() overrides of huge_string and huge_binary
  (readability-convert-member-functions-to-static) and the non-const
  test value (misc-const-correctness); mark them like the #5584 types
- clang with libstdc++ 10 cannot compile the file for C++17: the
  std::filesystem::path conversion considered for huge_string, a class
  derived from std::string, is ambiguous. Guard it with
  JSON_TEST_BEYOND_UINT32_STRING, which #5584 introduced for the same
  reason, and define that macro before both test blocks.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 17:50:19 +02:00
Niels Lohmann 1e101ecac1 Add BON8 support (#2998)
* Add BON8 support

Add to_bon8/from_bon8 and input_format_t::bon8 for BON8, a binary format
that uses the byte values that cannot begin a UTF-8 character as type
markers, so strings need no length prefix. It is the most compact of the
supported binary formats on the benchmark files.

The reader is non-recursive like the other binary readers. A string ends
at the first byte that cannot continue it, so the reader hands the one or
two bytes it reads past a string back to the value that follows. The
writer produces the canonical representation of the specification, except
for NFC normalization; its output is identical to that of the reference
implementation (HikoGUI) on all files of the test data.

The round-trip tests need the .bon8 files of json_test_data 3.2.0.

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

* Address review comments

- Reuse detail::validate_one_utf8 to check strings in to_bon8; the error
  now names the first byte of the invalid sequence.
- Document that to_bon8 leaves bytes in the output adapter on an
  exception, and that string_open is only an output of write_bon8_marker.
- Explain why the pushback buffer of the BON8 reader cannot overflow.

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

* Select the BON8 float prefix by type

get_bon8_float_prefix only depends on the type of its argument, so make
the type a template parameter instead of passing an unused value.

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

* Rename a test variable that Flawfinder mistakes for read()

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

* Fix the BON8 CI failures

- compare the float in write_bon8_float with number_float_t constants,
  so GCC does not warn about a float-to-double conversion
- mark check_bon8_utf8's context as used when exceptions are disabled
- choose the compact float prefix in a helper rather than with nested
  conditional operators (clang-tidy)
- use auto for the cast in the BON8 integer reader (clang-tidy)
- write the int32 minimum test values as long long literals (MSVC C4146)

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

* Amalgamate

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

* Read BON8 strings in bulk from contiguous input

- copy the valid UTF-8 of a string in one step when the input is
  contiguous (twitter.json is read in 1.68 instead of 2.52 ms,
  jeopardy.json in 196 instead of 297 ms, close to CBOR and MessagePack)
- share the new valid_utf8_prefix() with the writer's UTF-8 check, which
  now skips ASCII 8 bytes at a time
- let the fuzzer check that contiguous and stream input give the same
  value or error, and test both paths in the unit tests
- clarify that a second 0xFF after a string is an empty string

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

* Link the BON8 functions from the other binary format pages

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

* Name the bulk scan flag after the input, not BON8

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

* Read BSON keys in bulk from contiguous input

BSON keys (and array indices) are C-style strings, which were read byte
by byte. For contiguous input they are now read up to their \x00-byte in
one step, using the same bulk_scan flag as BON8 strings: twitter.json is
read in 1.46 instead of 2.01 ms, citm_catalog.json in 2.93 instead of
3.33 ms, jeopardy.json in 182 instead of 207 ms. canada.json, whose keys
are almost all one-digit array indices, takes 2 % longer.

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

* Fix the BON8 CI failures of the bulk-read tests

- skip the contiguous-versus-stream tests of BON8 strings and BSON keys
  when exceptions are disabled: they catch the parse errors of invalid
  input, and without exceptions the library aborts instead
- use static_cast for the int64 test value (google-readability-casting)

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

* Move the explicit basic_json instantiation into its own test file

Linking test-regression3_cpp20 with clang and MinGW failed with
"relocation truncated to fit: IMAGE_REL_AMD64_REL32 against `.rdata'",
as test-regression2 did before #5511. The explicit instantiation of
basic_json<> for #4825 compiles every member function, including the
BON8 reader and writer, into that object, and it was already close to
the limit (2,226,104 bytes on develop, 2,234,960 with BON8; clang -O1,
C++20).

Give the instantiation a file of its own: unit-regression3 is now
1,594,736 bytes and unit-explicit_instantiation 1,095,064. The new file
mentions JSON_HAS_CPP_17 and JSON_HAS_CPP_20 so it keeps being built
for the C++17 standard the regression was about.

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

* Convert the bytes of the BON8 test strings explicitly

The str() helper constructed a std::string from a byte range, which
converts each unsigned char implicitly; -fsanitize=integer reports that
for bytes of 0x80 and above (ci_test_clang_sanitizer).

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 16:56:21 +02:00
Niels Lohmann f682cd2ef1 Skip the #5515 MessagePack size tests on 32-bit platforms (#5590)
The tests fake a container size of UINT32_MAX + 1, which does not fit
into a 32-bit std::size_t: MSVC rejects the truncation (C4305/C4309
with /WX), and clang-cl wraps the size to 0 so nothing throws. Guard
them with SIZE_MAX > UINT32_MAX like the tests from #5584.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 15:58:59 +02:00
Kartikey Negi 6bd106893a Fix CBOR tag handling in cbor_tag_handler_t::store for non-binary items (#5559)
When using cbor_tag_handler_t::store, tags 0xD8-0xDB previously assumed
that the tagged item was a byte string, unconditionally attempting to
parse binary data and failing on valid CBOR documents containing tags
applied to integers, strings, arrays, or objects (such as self-describe
tag 55799).

Check whether the tagged data item is a byte string (0x40-0x5B or 0x5F).
If it is a byte string, store the subtype on the binary value as before.
Otherwise, iteratively process the tagged value in the driver loop using
item_read so that chained tags do not consume native stack space.

Part of #5316.

Signed-off-by: ReturnKartikey <kartikeynegi2000.work@gmail.com>
2026-09-27 14:28:55 +02:00
bucketbase26 98e00d22e5 Cut test suite runtime in binary roundtrips and integer sweeps (#5519)
* Cut test suite runtime in binary roundtrips and integer sweeps

The Linux CI jobs pass --no-skip, so skip() does not help there.
Parse each corpus file once in the binary roundtrip loops instead of
four times. Sample the 16-bit integer ranges with stride 7 (still hits
every low byte) and always keep the endpoints.

Also drop the 5M-node parse test to 500k, which still covers the
non-recursive destructor, and move jeopardy.json into its own skipped
test so the cheaper binary-format size checks actually run.

See #5418.

Signed-off-by: ayush-singh-0601 <singhayush062006@gmail.com>

* Drop useless int32_t casts in the sampled integer loops

ci_test_gcc compiles with -Werror=useless-cast. On that compiler
int32_t is int, so static_cast<int32_t> of the loop bound is an
error. The bounds are already int, and the sampled values do not
change.

Signed-off-by: ayush-singh-0601 <singhayush062006@gmail.com>

* Revert unit-binary_formats.cpp to develop and fix comment

Revert tests/src/unit-binary_formats.cpp to its develop state.
The test-case split made valgrind jobs slower instead of faster,
because the cheaper corpus files (canada/twitter/citm/sample)
now ran under valgrind where they never did before.

Fix the next_integer_sample comment: the function has no 'first'
parameter, so describe what the function actually does.

Signed-off-by: ayush-singh-0601 <singhayush062006@gmail.com>

---------

Signed-off-by: ayush-singh-0601 <singhayush062006@gmail.com>
2026-09-27 14:28:38 +02:00
Niels Lohmann f7972970a4 Throw instead of writing MessagePack lengths beyond UINT32_MAX (#5584)
* Throw instead of writing MessagePack lengths beyond UINT32_MAX

MessagePack stores the length of a string, binary value, array, or
object in at most 32 bits. For a larger value, to_msgpack wrote no length
at all, so the output could not be read back. It now throws
out_of_range.412, which BSON already uses for its 32-bit length fields.

The check lives in one function, so each length is written by an
if/else chain that ends in a plain else, without a condition that can
never be false. It is tested with string and binary types that report a
size beyond UINT32_MAX without allocating it, like the BSON tests do.

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

* Fix the CI failures of the MessagePack length check

- mark to_msgpack_length's value as used when exceptions are disabled
  (-Wunused-parameter, misc-unused-parameters)
- put "Exception safety" before "Exceptions" in to_msgpack.md, as the
  documentation style check requires
- create the test's string value from its type: constructing it from a
  beyond_uint32_string_t considers the std::filesystem::path conversion,
  which libstdc++ 10 reports as ambiguous for a class derived from
  std::string (clang 13)

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

* Skip the MessagePack string length test for clang with libstdc++ 10

C++17 builds consider the std::filesystem::path conversion for the
string type, and with clang and libstdc++ 10 that conversion is
ambiguous for a class derived from std::string. Creating the value from
its type did not avoid it, since any basic_json with that string type
instantiates the check. The binary and ext cases are still tested there.

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

* Keep the MessagePack string test type and its alias in one block

astyle indented the alias oddly when it had an #ifdef of its own after
the binary alias; declare it right after the string type, in the same
block.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 14:28:16 +02:00
Niels Lohmann 6178982b8d Compare unordered objects by key below the nesting bound (#5582)
* Compare unordered objects by key below the nesting bound

Values nested deeper than the nesting bound are compared without the
call stack, walking both objects entry by entry. Two equal objects of a
type that enumerates its entries in no fixed order - std::unordered_map,
say - can be walked in different orders, so they compared unequal, and
a deep copy compared unequal to its original. std::unordered_map's own
operator== does not depend on the order, which is what applies above the
bound.

Where the keys differ, equality now finds the entry by its key instead.
An ordering, and ordered_map, whose operator== compares its entries in
sequence, still decide by the key.

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

* Test unordered object equality without std::unordered_map

basic_json<std::unordered_map> instantiates std::pair<const string,
basic_json> while basic_json is still incomplete. The standard does not
require std::unordered_map to support that, and libstdc++ 6 to 9 as well
as the EDG front ends of icpc and nvc++ reject it, which broke the build
of unit-comparison on those CI jobs.

The test now uses an object type derived from std::map (which, as the
default object type, works everywhere) whose comparator orders keys
ascending or descending as chosen at construction, and whose operator==
does not depend on the order of the entries - the property of
std::unordered_map the test is about.

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

* Compare the test object type's entries with std::all_of

clang-tidy (readability-use-anyofallof) asked for std::all_of instead of
the loop in unordered_object_t's operator==. The entry type is spelled
out, as C++11 needs typename for base_type::value_type and C++20
reports it as redundant.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 14:21:58 +02:00
Niels Lohmann 85f8b21e1c Add tests for uncovered code paths (#5581)
* Add tests for uncovered code paths

Cover code the test suite did not reach, found from the Coveralls report
of develop and a local coverage run of HEAD:

- dump() of every kind of value below the bound of the recursive descent
  (pretty-printed objects, binary values, discarded values, scalars), and
  flushes of the escape and write buffers mid-string and mid-binary
- the iterative comparison: objects with different keys, containers that
  are a prefix of each other, and elements that cannot be ordered, each
  both at the top level and below the nesting bound
- SAX handlers that stop at any event, including the end of a nested
  container, in the BSON, CBOR, MessagePack, UBJSON and BJData readers
- from_bson/cbor/msgpack/ubjson/bjdata returning a discarded value
  through the iterator and pointer overloads
- JSON Patch, diff, merge_patch and update(..., true) on ordered_json
- smaller gaps: get_allocator(), to_ubjson/to_bjdata into a string,
  value() with an unresolvable JSON pointer, integer/float comparison
  below the integer range and with negative fractions, conversion to a
  custom binary type, std::formatter::parse on a spec without '}',
  unescape() of a lone '~', and the callback parser's start_array()

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

* Cover more paths that were thought unreachable

- parse_float_fast() declining malformed or inexact input, called
  directly since the lexer only passes well-formed numbers to it
- a UTF-16 high surrogate followed by a unit above the low surrogates
- self-assignment of a const_iterator
- a truncated CBOR string read through non-contiguous iterators
- serializing a long double under the de_DE locale, which undoes the
  locale's decimal point and thousands separator
- values read from a binary format carrying no diagnostic positions,
  with and without a parser callback

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

* Fix the CI failures of the new coverage tests

- declare the self-assignment reference const (misc-const-correctness)
- expect the (/path) prefix that JSON_DIAGNOSTICS adds to the messages
  of the failing ordered_json patch operations

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

* Expect the byte range JSON_DIAGNOSTIC_POSITIONS adds to the patch errors

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

* Build the expected dump of the nested-object test with +=

clang-tidy (performance-inefficient-string-concatenation) reported the
chain of operator+ calls that assembled the expected indented output.

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

* Compare the BJData and UBJSON test outputs byte by byte

Building a std::string from the byte vector converts each byte
implicitly, which -fsanitize=integer reports for bytes of 0x80 and
above (ci_test_clang_sanitizer).

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 14:19:22 +02:00
Niels Lohmann 4fa95d9810 Remove unreachable branches from the binary writer (#5583)
Coverage reported conditions in the binary writer that can never be
false, and marked the code behind them with LCOV_EXCL. Remove them
instead of excluding them:

- CBOR writes the length of a string, binary value, array, or object
  exactly like an unsigned integer, only with another major type. One
  function, write_cbor_head(), now writes both, so the integer tests
  cover every width and the four excluded 64-bit length branches are
  gone.
- A last `else if` whose condition holds for every remaining value
  (an unsigned value at most UINT64_MAX, a signed one in the range of
  int64_t) is now a plain `else`.
- Whether a signed integer fits into an int64 for UBJSON and BJData is
  decided by its type at compile time. Only an integer type wider than
  64 bits gets a range check and the high-precision fallback.
- The private get_impl(boolean_t*) was never called.

The UBJSON type prefix 'H' of an optimized container of unsigned
integers beyond the range of int64 was reachable although excluded; it
is tested now.

The output is unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-27 14:18:46 +02:00
Dadi Reddy Sai Praneeth Reddy fe4a544c7e handled when size exceed uint32 (#5515)
* handled when size exceed uint32

Signed-off-by: dsp0redy <saipraneethreddy.dadireddy@gmail.com>

* addressed review comments

Signed-off-by: dsp0redy <saipraneethreddy.dadireddy@gmail.com>

* updated unit test

Signed-off-by: dsp0redy <saipraneethreddy.dadireddy@gmail.com>

* added amalgamation patch

Signed-off-by: dsp0redy <saipraneethreddy.dadireddy@gmail.com>

---------

Signed-off-by: dsp0redy <saipraneethreddy.dadireddy@gmail.com>
2026-09-27 14:17:32 +02:00
dependabot[bot] f422b753cc Bump the codeql-action group across 1 directory with 4 updates (#5576)
Bumps the codeql-action group with 4 updates in the / directory: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/autobuild](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action).


Updates `github/codeql-action/init` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `github/codeql-action/autobuild` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `github/codeql-action/analyze` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

Updates `github/codeql-action/upload-sarif` from 4.38.0 to 4.38.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/b96794f015dfd88f77b49b1c93e0fa7110f94c63...1c5b675653bb5c22dbe9b12b556ec555138e09fd)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.38.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: codeql-action
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-25 21:57:28 +02:00
Niels Lohmann 95e9a5931c Write BSON in linear time, without recursing per nesting level (#5553)
* Write BSON in linear time, without recursing per nesting level

to_bson() had two problems with nested values:

- It recursed once per nesting level, so a value nested deeply enough -
  100,000 levels on an 8 MiB stack - exhausted the call stack and
  terminated the process, although parse() accepts such values without
  complaint.
- BSON prefixes every document and array with its length. The writer
  computed that length by walking the entire value below it, again for
  every nested document it wrote, which made serializing O(size x depth).
  A 200-level document took 30 ms instead of 1.

Both passes are now iterative, and each length is computed exactly once:

- calc_bson_sizes() computes the length of every document and array in
  one pass, each from the lengths of its entries, into a table ordered
  the way they are written.
- write_bson_document() then writes the document, taking each length from
  the table.

Everything observable is unchanged, as a differential test against
develop confirms byte for byte:

- The same bytes are written.
- A key containing U+0000 still throws out_of_range.409 for the same
  first key, with the same diagnostics path, before anything is written.
- A document too large for BSON still throws out_of_range.412 before
  anything is written.
- A binary subtype above 255 still throws out_of_range.415 after the
  same partial output.

Only the enclosing objects and arrays are kept on a stack, so a flat
document allocates nothing for it. Measured against develop (clang -O3,
median of 201 runs): flat objects unchanged, flat arrays 37% faster (the
array length was computed twice), a nested 3,000-object document 2x
faster, a 200-level document 33x faster.

to_bson.md documented the quadratic complexity since #5334; it is linear
again.

Fixes #5392 for BSON, and #5308.

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

* Do not require a default-constructible string_t in the BSON writer

GCC 4.9 and MSVC rejected the test's huge_string_t, which has no default
constructor; develop never default-constructed string_t here either.

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

* Let the BSON index-name helper only fill its output parameter

It returned a reference to the string it filled, so callers held a second
name for index_name. Addresses review feedback.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 21:56:39 +02:00
Niels Lohmann 1e44262091 Make JSON_STRICT_NUL_HANDLING part of the ABI tag (#5560)
* Make JSON_STRICT_NUL_HANDLING part of the ABI tag

JSON_STRICT_NUL_HANDLING (#5534) changes the bodies of inline functions:
the lexer's handling of '\0' and input_adapter() for char arrays. So
translation units compiled with and without it define the same functions
differently, an ODR violation - the case the ABI tag exists for, as with
JSON_BRACE_INIT_COPY_SEMANTICS (_bics). It now appends _snul to the inline
namespace. The macro is new in 3.13.0, so no existing namespace changes.

Its default moves to abi_macros.hpp, and it is only #undef'd without
JSON_TEST_KEEP_MACROS, as for the other ABI macros. The ABI config tests,
the namespace docs, the macro's docs and the Natvis file cover the new tag.

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

* Amalgamate

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 21:54:11 +02:00
Niels Lohmann c60a0bc336 Allocate the deep copy's key scratch space with the provided allocator (#5573)
* Allocate the deep copy's key scratch space with the provided allocator

The iterative deep copy builds each object's keys in a temporary vector of
key/value pairs before handing them to the object's range constructor. That
vector holds basic_json values, so like the values themselves it now uses
AllocatorType instead of std::allocator.

Also document that AllocatorType covers the JSON values, while most
temporary storage still uses std::allocator.

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

* Count allocate_at_least in the scratch-counting test allocator

From C++23 on, libc++'s containers allocate through allocate_at_least when
the allocator has one. The test allocator inherited it from std::allocator,
so the scratch allocations were not counted and the test failed on Xcode.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 21:53:49 +02:00
Niels LohmannandClaude Sonnet 5 d19f7f5dce Fix BSON conformance issue (#5185)
* 🐛 fix BSON conformance issue

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

* 🐛 fix BSON conformance issue

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

* 🐛 reject ill-formed UTF-8 in CBOR/MessagePack/BSON text strings at decode time (#5531)

from_cbor()/from_msgpack()/from_bson() copied the raw bytes of a decoded
text string into the resulting json value without any UTF-8 validation,
even though RFC 8949 §3.1 (CBOR) and the MessagePack/BSON specifications
all require text strings to be valid UTF-8. Malformed input only failed
later, if the value was dump()'d, with a type_error.316 - so the
allow_exceptions=false pattern used specifically to get a discarded
sentinel instead of an exception did not discard this category of
malformed input, unlike every other kind of malformed binary input this
library rejects at decode time (see #5529).

Fix this at the single choke point shared by BSON/CBOR/MessagePack/UBJSON
string reads, binary_reader::get_string(): validate the bytes with the
UTF-8 DFA right after they are read, and report failures the same way as
every other binary_reader error (parse_error.113), so allow_exceptions
and strict discarding behave consistently. get_binary()/binary blob reads
are untouched and still accept arbitrary bytes, since only text strings
are required to be UTF-8.

There were two independent implementations of a UTF-8 validator: the
lexer's streaming scanner, and the serializer's Hoehrmann DFA used by
dump_escaped_impl(). Rather than write a third, the serializer's decode()
function, its utf8d table and the UTF8_ACCEPT/UTF8_REJECT constants are
extracted into detail/string_utils.hpp (a low-level header already
included before both detail/input/ and detail/output/), alongside a new
is_valid_utf8() helper built on the same decode() step. serializer.hpp's
dump_escaped_impl() now calls the shared decode(), so there is exactly
one UTF-8 validator in the codebase; dump()'s exact type_error.316
messages and byte-index reporting are unchanged (see the added
regression-guard test in unit-serialization.cpp).

Claude-Session: https://claude.ai/code/session_01N4RQ1Ahan5YAGbnAQGjZTY

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* ⚡ validate only newly read bytes of binary-format strings

get_string() validated the whole result after each call, but get_bytes()
appends to it and CBOR indefinite-length strings collect all chunks in
the same result, so every chunk re-validated everything read before it.
An input of many small chunks took quadratic time (80000 one-byte chunks,
160 KB of input, took about 7 seconds). Only the newly read bytes are
validated now, which also matches RFC 8949's requirement that every
chunk is valid UTF-8 on its own.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 20:45:28 +02:00
Niels Lohmann 632a5812a8 Support zero-member types in NLOHMANN_DEFINE_TYPE_* macros (#4041) (#5272)
* Support zero-member types in NLOHMANN_DEFINE_TYPE_* macros (#4041)

NLOHMANN_DEFINE_TYPE_INTRUSIVE(Type) and its 11 sibling macros produced
broken code for types with no members to serialize. Invoking a variadic
macro so __VA_ARGS__ is empty is only standard-conforming since C++20,
so a plain __VA_OPT__ fix (as tried in #5142) breaks every pre-C++20
build under -pedantic. Instead, make all 12 macros purely variadic and
dispatch on argument count using a sentinel-padded extension of the
existing NLOHMANN_JSON_GET_MACRO idiom, giving full C++11-C++26 support
with no feature-test gate.

Verified against real GCC 16 and Clang at -std=c++11/14/17/20 with
-pedantic -Werror -Wvariadic-macros: zero regressions in the existing
unit-udt_macro.cpp suite plus 12 new zero-member test cases.

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

* Fix CI failures in zero-member NLOHMANN_DEFINE_TYPE_* macros

Three issues surfaced on PR #5272's real CI that weren't caught by
local testing against a narrower flag set:

- GCC -Werror=noexcept: the four truly-empty from_json bodies (plain
  INTRUSIVE/NON_INTRUSIVE, with and without _WITH_DEFAULT) provably
  never throw but weren't declared noexcept; mark them noexcept
  explicitly. to_json and the derived-type from_json overloads are
  left alone since they genuinely can throw (object assignment /
  delegating to the base class's from_json).
- clang-tidy bugprone-macro-parentheses: false positive on the same
  8 zero-member bodies (Type/BaseType used purely as declarator
  types); suppressed with NOLINTNEXTLINE comments in the same style
  already used elsewhere in this file (see NLOHMANN_JSON_SERIALIZE_ENUM).
- MSVC's traditional preprocessor doesn't fully expand
  NLOHMANN_JSON_CAT(prefix, NLOHMANN_JSON_TYPE_TAG(...))(...) in one
  pass, which broke a pre-existing one-member usage in
  unit-regression2.cpp with syntax errors. Wrap all 12 public
  dispatcher macros in an extra outer NLOHMANN_JSON_EXPAND(...),
  matching the pattern NLOHMANN_JSON_PASTE already uses for the same
  MSVC quirk.

Re-verified against real GCC 16 and Clang at -std=c++11/14/17/20 with
-pedantic -Werror -Wvariadic-macros -Wnoexcept, including the exact
files that failed in CI (unit-udt_macro.cpp, unit-regression2.cpp),
against both the modular headers and the re-amalgamated single header.

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

* Fix clang-tidy misc-const-correctness in unit-udt_macro.cpp

The four zero-member ONLY_SERIALIZE test objects are only ever read
(via to_json), never mutated, so mark them const per clang-tidy.

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

* Fix derived-type macro dispatch capping members at 62 instead of 63

NLOHMANN_JSON_GET_MACRO resolves 64 positional arguments, with NAME at
position 65. NLOHMANN_JSON_TYPE_TAG dispatches on Type plus the member
list, so it resolves correctly up to the 63 members NLOHMANN_JSON_PASTE
supports. NLOHMANN_JSON_DERIVED_TYPE_TAG dispatched on the two-token
Type,BaseType prefix plus the member list, running out one slot early:
at 63 members, position 65 landed on the last member name instead of a
sentinel and NLOHMANN_JSON_CAT built an undefined identifier such as
NLOHMANN_JSON_DEFINE_DERIVED_TYPE_INTRUSIVE_m63, with the compiler
reporting "unknown type name 'm1'" once per member and nothing pointing
at an argument-count limit.

That silently reduced all six NLOHMANN_DEFINE_DERIVED_TYPE_* macros from
63 members to 62, contradicting the "up to 63 members" contract in
docs/mkdocs/docs/api/macros/nlohmann_define_derived_type.md.

Drop the leading Type and defer to NLOHMANN_JSON_TYPE_TAG so the tag is
computed from BaseType plus the member list, which fits the available
slots. The zero-own-member derived bodies are therefore selected by tag
1 rather than 2, and the sentinel table for the derived tag is no longer
needed.

Add a regression test at the documented maximum for both the plain and
the derived macros; it fails to compile against the previous dispatch.

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

* Name the zero-member macro bodies by intent, not argument count

The dispatch tag was the literal token 1 or N, pasted onto a macro prefix
to select the zero-member or member-carrying body. For the derived-type
macros that reads wrong: their tag is computed after dropping the leading
Type, so the zero-member body was named _1 while taking two parameters
(Type, BaseType).

Emit EMPTY and MEMBERS instead. The mechanism is unchanged -- the tag is
still a token pasted onto the prefix by NLOHMANN_JSON_CAT -- but the body
names now say what they are rather than encoding an argument count that
only lines up for half of the macros.

Collapse the four duplicated zero-member bodies while here: with no
members there is nothing to default, so each _WITH_DEFAULT_EMPTY body was
a byte-for-byte copy of its plain counterpart. They are now one-line
aliases, leaving a single definition of what an empty object serializes
to per intrusive/non-intrusive and base/derived combination.

No functional change: for both zero-member and member-carrying types the
preprocessed to_json/from_json output is token-for-token identical, and
the arity limits are unchanged (63 members, base and derived).

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

* Document zero-member support in the macro API reference

docs/mkdocs/docs/features/arbitrary_types.md already gained a note, but
the three api/macros pages are where the parameter contract is actually
specified and they still described member as a non-empty list.

State that the list may be empty on each page, and add a note showing
what the zero-member case generates: an empty JSON object for the plain
macros, and base-type-only serialization for the derived ones. Both notes
record that the WITH_NAMES variants do not support this.

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

* Keep user macros named EMPTY or MEMBERS out of the member-count dispatch

The dispatch produced the bare token EMPTY or MEMBERS and pasted it onto
the macro prefix afterwards. In between, the token was rescanned, so a
user macro with either name replaced it: with `#define MEMBERS x` in
scope, even NLOHMANN_DEFINE_TYPE_INTRUSIVE(A, member) -- which compiled
before -- expanded to garbage, and `#define EMPTY` broke the zero-member
form.

Paste the suffix onto the prefix directly in the GET_MACRO slot table
instead. Operands of ## are not macro-expanded, so the selected body name
is formed before any user macro can interfere. NLOHMANN_JSON_TYPE_TAG and
NLOHMANN_JSON_DERIVED_TYPE_TAG become NLOHMANN_JSON_TYPE_BODY and
NLOHMANN_JSON_DERIVED_TYPE_BODY, taking the prefix as their first
argument; NLOHMANN_JSON_CAT is no longer needed. The body macro names are
unchanged, and so is the generated code.

Add a regression test that defines EMPTY and MEMBERS around plain and
derived types, with and without members.

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

* Test for EMPTY and MEMBERS so -Wunused-macros accepts them

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 20:44:30 +02:00
Niels Lohmann ec4bdc398a Document the benchmarks, make them build and compare versions again, and pin Google Benchmark (#5556)
* Document the benchmarks, and make them build and compare versions again

The benchmark project hasn't configured since #4793: download_test_data.cmake
compiles cmake/detect_libcpp_version.cpp relative to CMAKE_SOURCE_DIR, which
is tests/benchmarks when that is the top-level project, so try_run fails and
so does `make run_benchmarks`. The path is now relative to the module itself,
which is the same file for the main build.

The Dump benchmark discarded dump()'s result, which is [[nodiscard]] by now;
it warned, and left the optimizer free to shorten the loop. The result is
now kept with benchmark::DoNotOptimize.

A new cache variable, JSON_BENCHMARK_INCLUDE_DIR, names the directory holding
the nlohmann/json.hpp to benchmark (single_include by default, as before),
so the same benchmarks can be built against two versions and compared.

tests/benchmarks/README.md documents what is measured, how to build and run
the benchmarks, how to read the output, and how to compare two versions with
Google Benchmark's compare.py; it recommends doing so by hand before a
release rather than in CI.

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

* Point ci_benchmarks at tests/benchmarks

The target has configured ${PROJECT_SOURCE_DIR}/benchmarks since it was
added in #2561, but the benchmarks live in tests/benchmarks.

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

* Pin Google Benchmark to release 1.9.5

The benchmarks fetched Google Benchmark's main branch, so two builds on
different days could measure with different library code, and CMake 3.30
and later warn that the single-argument FetchContent_Populate() is
deprecated. Fetch the 1.9.5 release archive, verified by its SHA-256,
with FetchContent_MakeAvailable() instead. That needs CMake 3.14; Google
Benchmark itself already needed 3.13.

Its -Werror is switched off, so a newer compiler's new warnings cannot
break the pinned release, and its install rules are no longer added.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 20:43:34 +02:00
Niels Lohmann a9ab2a62ba Cancel superseded runs of the remaining workflows (#5579)
Ubuntu, Windows, macOS, and CodeQL already cancel an older run of the same
workflow on the same ref. Check amalgamation, CIFuzz, Dependency Review,
Flawfinder, Semgrep, Scorecard, and the labeler did not, so every push
to a pull request left their earlier runs going. Give them the same
concurrency group. The labeler runs on pull_request_target, where
github.ref is the base branch, so it groups by pull request number.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 20:40:28 +02:00
Niels Lohmann ad2c14b985 Document response times, supported versions, access, secrets, and dependency policies (#5580)
Answer the OpenSSF Best Practices criteria that asked for policies the
project follows but had not written down:

- SECURITY.md: a first response within 14 days, publishing an advisory
  with credit once a fix is released, and that only the latest release
  receives security fixes.
- Governance: who has access to the project's resources, how write or
  admin access is granted, and how CI secrets are stored and rotated.
- Quality assurance: how dependencies of the build, test, and
  documentation tooling are pinned, scanned, and kept free of known
  vulnerabilities.

Also update the assurance case, since comparison no longer recurses per
nesting level (#5390), and point the best practices badge and links to
bestpractices.dev under the program's current name.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 20:34:58 +02:00
Niels Lohmann e1310ad43c Fix CI: resolve clang-tidy findings in the stream position tests (#5578)
#5344 added two lines to unit-deserialization.cpp that clang-tidy
reports: modernize-return-braced-init-list for the remaining() helper
and readability-isolate-declaration for "json j1, j2, j3;".

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 20:19:43 +02:00
Alexander LaninandNiels Lohmann 465407f3ce Improve error message for const fields (#2818)
* Improve error message for const fields

* Reject const arguments to get_to() with a clear message

Reword the static_assert, add it to the C array overload of get_to() as well,
and document that v must not be const.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Co-authored-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 18:02:38 +02:00
Niels Lohmann 02dd3e67f2 Fix stack overflow and exponential runtime when comparing nested values (#5390)
* Compare values without recursing, and without comparing them twice

Comparing two values compared their containers, which compare their elements,
which brought the comparison back once per nesting level. Two values nested
deeply enough exhausted the call stack and terminated the process with a
segmentation fault - the same bug as #5387, in the last operation that still
had it.

Worse, an ordered comparison took exponentially long in the nesting depth
before C++20. std::vector's operator< is a lexicographical comparison, which
asks whether an element is less than its counterpart and then whether the
counterpart is less than it - two full comparisons of everything below that
element, at every level. Comparing two equal values nested 30 levels deep,
which is nothing unusual, took 3.8 seconds; 40 levels would have taken an
hour, and nothing about the value has to be pathological to get there. C++20
is unaffected: std::lexicographical_compare_three_way asks once.

Compare a value that is nested too deeply to descend into on an explicit
stack instead, in a single pass that yields less, equal, greater or unordered
at once. Equality and the three-way comparison descend as they always did for
the first 128 levels, which nothing measurable costs them; an ordered
comparison no longer descends at all, which is what takes the exponent out of
it. Objects and arrays that are not nested deeply are otherwise compared
exactly as before.

The results are unchanged for every pair of values: 68121 comparisons of a
corpus that covers NaN, discarded values, mixed number types, binary values,
empty containers and both object types are identical to develop, in C++11,
C++17 and C++20, with and without thread_local storage and legacy discarded
comparison. Reproducing that meant reproducing two subtleties: a lexicographic
comparison steps over a pair it cannot order, where a three-way comparison
stops at it, and an object compares its keys with < where its entries are
ordered but with == where they are only checked for equality - not with the
object's own comparator, which for nlohmann::ordered_map tells equality.

Equality needs no ordering, so it no longer asks for any: a key or string type
that can only be compared for equality still works.

Measured (medians of 7 interleaved runs, clang -O3, C++11): comparing two
equal values nested 30 levels deep 3778 ms -> 0.002 ms; ordering flat objects
-33.6%; ordering flat arrays of numbers +27.3%, the one shape that pays for
the single pass; equality unchanged throughout.

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

* Describe comparison in the no-thread-local docs and CI target

Comparing two values now bounds its descent with a thread_local counter
just as copying does, so the JSON_NO_THREAD_LOCAL page, the macro
overview and the ci_test_no_thread_local target cover both rather than
copying alone.

Also record what switching the macro on costs a comparison: on the
benchmark documents, comparing two equal values takes 10% to 90% longer.

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

* Take the descent flag as an argument rather than testing it

MSVC reports the test of a constant as C4127 ("conditional expression is
constant"), which the Windows builds treat as an error: may_descend is
false for operator<, so the operand short-circuits the whole condition.

Passing it to compare_descent_exhausted() puts the test where the value
is an ordinary parameter, and leaves the call sites with no condition of
their own.

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

* Note the comparison fallback in the no-thread-local documentation

The macro page describes what the library defines JSON_NO_THREAD_LOCAL for
by itself in terms of copying alone; comparing falls back the same way.

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

* Parenthesise the reserve() computation in the comparison test

clang-tidy reports the mixed * and + as readability-math-missing-
parentheses, as it does for the identical line in the copy test.

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

* Use the shared descent bookkeeping rather than a second set

Comparing kept a thread_local count, a limit and a guard of its own beside
the ones copying already had, all three the same thing under a different
name. They are gone; the shared count, limit and guard do the work.

The guard grows a second constructor here, because the comparison
operators are written as a macro and a macro cannot use the preprocessor:
it cannot look the count up behind an #ifdef the way copy_structured does,
so the guard looks it up for it. nesting_depth_exhausted() arrives for the
same reason - whether an operator descends at all is a constant at every
call site, and testing it there is what MSVC reports as C4127.

Also say in compare_leaves what happens to a pair that is an array on one
side and an object on the other, since the answer is not obvious from the
code: an operator only descends into two values of the same type, so such
a pair is told apart by its types alone - unequal, and ordered the way the
types are - exactly as it is above the bound.

And record what the explicit stack costs: the comparison operators are
noexcept and the container comparison this replaces allocated nothing, so
running out of memory here ends the process instead of throwing. It takes
a value nested past the bound and an exhausted heap to reach, and the same
comparison used to exhaust the call stack, but it is a new way to fail.

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

* Amalgamate

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 17:58:59 +02:00
Niels Lohmann abbe52d6de Add JSON_PRECISE_STREAM_POSITION to leave the character that terminates a number in the stream (#5344)
* docs: qualify the operator>> stream positioning guarantee

operator>>'s notes state that it leaves the stream positioned right
after the parsed value, so that concatenated JSON values can be read
back to back. That does not hold when the value is a number: a number
is only terminated by the character that follows it, and the lexer's
unget() is simulated (it rewinds only the lexer's own bookkeeping),
so that character stays consumed from the stream.

Document the actual behaviour: the guarantee holds for all value types
except numbers, which must be followed by whitespace. Also qualify the
cross-reference on the JSON Lines page, which repeated the unqualified
claim.

Documentation only; the behaviour itself is tracked in #5340.

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

* fix: restore the character that terminates a number (#5340)

operator>> is documented to leave the stream positioned right after the
parsed value, so that concatenated JSON values can be read back to back.
That did not hold for numbers: a number is only terminated by the
character following it, and lexer::scan_number() reads that character
and calls unget() -- which is simulated and rewinds only the lexer's own
bookkeeping. input_stream_adapter consumes via sbumpc() with no matching
sungetc(), so the terminating character stayed consumed and the next
extraction started one byte too late ('1true' left the stream at 'rue').

Propagating unget() to the adapter directly does not work: next_unget
makes the following get() replay the cached character, so the terminator
would be delivered twice. Instead, restore the still-pending character
once at the end of a non-strict parse, where the input is handed back to
the caller:

- input_stream_adapter gains unget_character() (sungetc()) and advertises
  it via supports_unget, detected the same way as supports_seek.
- lexer::restore_pending_unget() turns a pending simulated unget of a
  real (non-EOF) character into a real one and clears next_unget so the
  character is not also replayed. It is a no-op for adapters that cannot
  unget, and reports failure when sungetc() fails, in which case the
  input is left as it was before.
- parser calls it on the three non-strict paths, i.e. for operator>> and
  sax_parse(strict = false).

Strict parse()/accept() are unaffected: they require the input to end
after the value, so the character is consumed by the end-of-input check
anyway. Parse error messages and reported positions are unchanged.

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

* tests: fix CI failures in the #5340 test helpers

Four CI failures, all in the new test code:

- GCC (-Werror=useless-cast): drop the `json(...)` wrapper around
  `json::parse(...)`, which already returns a `json`.
- GCC (-Werror=unused-result): assign the discarded `json::parse()`
  result to a dummy, the idiom used elsewhere in the test suite, and
  catch `json::parse_error&` for consistency.
- clang-tidy (google-default-arguments): remove the default argument
  from the `pbackfail()` override; `sungetc()` supplies the base
  declaration's default.
- MSVC (bad allocation): `no_putback_streambuf::underflow()` set a
  one-character get area without advancing `m_pos`, so an implementation
  whose `istream::get` peeks before it bumps re-read the same character
  forever. Keep no get area at all: `underflow()` peeks, `uflow()`
  consumes, and `sungetc()` still always lands in `pbackfail()`, which
  is what the test needs.

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

* fix: leave the character that terminates a number in the input

Read the character following a number without consuming it, instead of
consuming it and putting it back. input_stream_adapter now peeks with
sgetc() and only steps over the character when the next one is requested
or when the adapter is destroyed, so releasing it cannot fail - no
putback position is required from the streambuf.

Suggested by gregmarr in #5344.

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

* docs: match the version history wording to the peek-based fix

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

* docs: drop the whitespace-separator caveat from the parsing pages

The caveat added in #5343 describes the behavior this branch fixes: a
number no longer consumes the character that terminates it, so
concatenated values need no separator.

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

* refactor: split the strict and non-strict paths in parser

Folding the release_lookahead() call into the existing strict check left
the "in strict mode" comment on an else-if branch, and made the strict
condition in sax_parse() redundant with the branch it followed.

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

* Put the stream position fix behind JSON_PRECISE_STREAM_POSITION

Leaving the character that terminates a number in the stream is observable:
reading "1,2,3" with repeated operator>> works today only because the comma
after each number is swallowed, and std::getline after a number skips the
line break. Both break with the fix, so make it opt-in for 3.x, as suggested
by @gregmarr in the review.

- JSON_PRECISE_STREAM_POSITION (default 0) selects the peek-based
  input_stream_adapter. Without it, the adapter is the consuming one from
  develop and has no supports_lookahead, so lexer::release_lookahead() and
  the parser's calls to it compile to nothing.
- The macro changes input_stream_adapter's layout and member functions, so
  it gets the ABI tag _psp, after _bics. The ABI config tests, the natvis
  generator, and nlohmann_json.natvis (regenerated) know the tag.
- The tests for the fix move to unit-precise-stream-position.cpp, which
  defines the macro itself and runs in every build, and gain the two cases
  above. unit-deserialization.cpp pins the default behavior instead.
- The docs describe the default behavior again and point to the new macro
  page; version history says "added in 3.13.0, planned default in 4.0.0".

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 17:56:18 +02:00
Niels Lohmann 98278dc3f6 Fix CI: disable MSVC warning C5285 for the vendored doctest (#5577)
The windows-11-arm runner now ships MSVC 19.51, which reports doctest's
forward declaration of std::tuple as C5285 ("cannot declare a
specialization for 'std::tuple'"). With /WX this breaks the msvc-arm64
job on develop and on every open pull request. Disable the warning for
the test targets, like the other MSVC warnings already disabled there.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 17:54:40 +02:00
Jeremy Nimmer 6c8ea0a6d1 Remove Bazel alwayslink=True (#5376)
This should have no effect for header only libraries as mentioned.

It was previously removed in e509007d but then accidentally added
again in 26cfec34.

Signed-off-by: Jeremy Nimmer <jeremy.nimmer@tri.global>
2026-09-25 08:46:19 +02:00
Niels Lohmann 01b53c8c15 Keep JSON_DIAGNOSTICS parent pointers of ordered_json members after erase() and update() (#5552)
* Keep JSON_DIAGNOSTICS parent pointers of ordered_json members after erase() and update()

ordered_json stores its members in a vector, and two operations moved
members without restoring their parent pointers afterwards:

- ordered_map::erase() re-constructs every member after the erased one in
  place. The basic_json move constructor leaves m_parent at nullptr, and
  none of the object branches of basic_json::erase() (by key, iterator, or
  iterator range) called set_parents(). This also affected merge_patch()
  with a null member and patch() with a remove operation.
- update() only set the parent pointer of the inserted member. Adding a key
  can reallocate the vector, which copies all other members and leaves
  their m_parent at nullptr. The set_parents() call added for #4813 only
  repaired this for the nested object of a merge, not for the target.

The next assert_invariant() on such an object (for instance, when copying
it) aborted, and diagnostic messages lost the path prefix above the moved
member. std::map-based json was not affected, because its nodes do not
move.

Erasing from an ordered_map object now calls set_parents(), and update()
uses set_parent(), which already refreshes all members for vector-based
objects. This makes the #4813 workaround redundant.

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

* Account for JSON_DIAGNOSTIC_POSITIONS in the ordered_json parent-pointer test

The merge_patch() case parses its input, so with JSON_DIAGNOSTIC_POSITIONS
the exception message also carries the byte range of the parsed value.

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

* Silence clang-tidy for the intentional copy in the ordered_json parent-pointer test

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

* Keep parent pointers when update() merges past its descent bound

The iterative path of update() only set the parent pointer of the member
it inserted, like the recursive one did before. It now uses set_parent()
too, so ordered_json members that move when a nested object grows keep
their parents, and the set_parents() calls that patched this up after
each nested merge are gone.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 08:29:36 +02:00
Niels Lohmann cc472af13f Check the fuzzers' UBJSON/BJData round-trip invariants in the unit tests (#5569)
* Check the fuzzers' UBJSON/BJData round-trip invariants in the unit tests

The strongest correctness checks for the UBJSON and BJData writers lived
only in the OSS-Fuzz drivers: anything from_ubjson()/from_bjdata()
returns must serialize with every option combination, parse back, and
re-serialize stably. Those checks only run at OSS-Fuzz, so regressions
surfaced days later as external reports - the same BJData assert pair
was reported five times over three years, and #5494's harness change
was followed by OSS-Fuzz 563659413 within a day.

Add "UBJSON round-trip invariants" and "BJData round-trip invariants"
test cases that run the drivers' checks on a fixed, deterministic corpus
(tests/src/round_trip_corpus.hpp): integer and float boundaries,
non-finite numbers, strings, binary values, optimized containers, deep
nesting, the JData annotated-array matrix, and seeded random containers.
They also check two properties the drivers do not: the first round trip
preserves the value, and re-serializing reproduces the exact bytes. For
BJData both exclude values containing a binary value, which is read back
as an array of integers unless it was written as a Draft 3 optimized
binary array; this carve-out is now documented in bjdata.md. Run against
the headers before #5542, the BJData test fails, including on the shape
from OSS-Fuzz 563659413.

Also document how OSS-Fuzz reports are handled (reference them as
"OSS-Fuzz: <id>", turn the reproducer into a unit test, keep drivers and
unit tests in sync) in tests/fuzzing.md, and link it from the PR
template and the quality assurance page.

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

* Add the OSS-Fuzz reproducers for 474400817 and 474480402 as unit tests

Following the convention added to tests/fuzzing.md, the reproducers of
the two BJData fuzzer asserts tracked since January are now unit tests:

- 474400817 (assert(false)): an empty object _ArraySize_ was written as
  the ND-array header length, which from_bjdata() could not read back.
  Fixed by #5455.

- 474480402 (to_bjdata(j2, false, false) == vec2): a one-byte Draft 3
  binary array is written in Draft 2 mode as a uint8 array and then
  re-serialized with the int8 marker. This is the documented exception to
  byte stability, not a library bug; OSS-Fuzz closed it after #5494
  relaxed the harness to value stability. The test pins the exact bytes
  so the exception stays deliberate.

The 563659413 reproducer is already a unit test (#5542). A comment also
ties the existing UBJSON excessive-count test to the timeout OSS-Fuzz
reported for that shape (testcase 6347769435193344).

OSS-Fuzz: 474400817
OSS-Fuzz: 474480402

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

* Fix GCC -Weffc++ and -Wuseless-cast warnings in the round-trip corpus

Initialize the atoms in the member initialization list, and drop the cast of
the generator's result, which already is std::size_t on 64-bit Linux.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 08:29:02 +02:00
Niels Lohmann 80bf54a5a2 Add a security assurance case to the documentation (#5572)
Describe the threat model, the trust boundaries, the secure-design
argument, and how common weaknesses are countered, with links to the
quality assurance page as evidence.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 08:28:34 +02:00
Niels Lohmann aada27405d Add a roadmap page to the documentation (#5571)
Describe what the project will and will not do over the next year,
and point to issue #3453 for the open question of a 4.0 release.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 08:28:16 +02:00
Niels Lohmann 3901b223e5 Complete the architecture documentation page (#5570)
* Complete the architecture documentation page

Replace the placeholder bullets and TODOs with a description of the
component pipeline (with a diagram), the source layout, the template
parameters, the value storage (now in struct data), the input and
output adapters, and the SAX interface.

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

* Link sources and basic_json, document full input adapter interface

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

* Align the default column of the template parameter table

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 08:28:00 +02:00
Niels Lohmann f290b36ad2 Fix CI: use VS 2026 on windows-11-arm and use raw string literals in tests (#5575)
The windows-11-arm runner image moved to windows-11-vs2026-arm64, which no
longer ships Visual Studio 2022, so the msvc-arm64 job failed at configure
time. Use the "Visual Studio 18 2026" generator like the msvc2026 job.

clang-tidy's modernize-raw-string-literal check flagged two string literals
in the nesting tests added by #5546 and #5547.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-25 08:23:53 +02:00
Niels Lohmann 4daca40d7b Merge deeply nested objects without recursing per nesting level (#5547)
* Merge deeply nested objects without recursing per nesting level

merge_patch() and update(j, true) merged a nested object by calling
themselves on it, once per nesting level. A value nested deeply enough -
50,000 levels of objects on an 8 MiB stack - exhausted the call stack
and terminated the process, although parse() accepts such values without
complaint.

Bound the descent the same way dump() does. The recursion now carries
the nesting level, and once merge_depth_limit() (128) levels have been
entered, update_members_iteratively() and merge_patch_iteratively()
finish the merge on an explicit stack. They still merge a nested object
completely before the next member, and in the same order, so the results,
including the parents JSON_DIAGNOSTICS reports paths from, are unchanged.
Values nested less deeply than the bound run the same code as before, so
the common case does not pay for the stack: merging only on it cost
10-14% in a first version.

The public signatures are unchanged. The recursive worker behind
merge_patch() has its own name rather than being a private overload, so
that &basic_json::merge_patch stays unambiguous.

Tests check every depth up to 300 against recursive reference
implementations of both operations, check the diagnostic paths past the
bound, and merge objects nested 100,000 levels deep.

Fixes #5545 for update(j, true), and #5393 for merge_patch().

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

* Use the shared recursion limit in update() and merge_patch()

merge_depth_limit() is gone in favor of detail::recursion_depth_limit().
The two identical function-local frame structs become one member struct,
merge_frame, with a constructor, so both loops emplace_back() their
frames. merge_patch_iteratively() copies the frame it works on out of the
stack and changes it only through stack.back().

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

* Build the update()/merge_patch() diagnostics test values instead of parsing them

Parsed values carry byte positions under JSON_DIAGNOSTIC_POSITIONS, which
the expected messages do not include.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-24 17:12:03 +02:00
Niels Lohmann 7c90ec2323 Hash deeply nested values without recursing per nesting level (#5546)
* Hash deeply nested values without recursing per nesting level

std::hash<basic_json> hashed an array or object by hashing each element,
which called detail::hash again once per nesting level. A value nested
deeply enough - 50,000 levels of objects on an 8 MiB stack - exhausted
the call stack and terminated the process. parse() accepts such values
without complaint, since the parser is iterative, and a parsed value is
hashed wherever it is used as a key in an unordered container.

Bound the descent the same way dump() does: detail::hash takes the
nesting level, and once hash_depth_limit() (128) levels have been entered,
hash_iteratively() hashes what is left on an explicit stack. It combines
the seeds in exactly the same order, so hash values are unchanged. A value
nested less deeply than the bound is hashed by the same code as before,
without allocating, and is as fast as before.

Tests check that every depth up to twice the bound hashes exactly like
the recursive definition of the hash, and that values nested 100,000
levels deep hash without crashing.

Fixes #5545 for std::hash.

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

* Declare hash_frame's constructor noexcept

GCC's -Wnoexcept (an error in CI) flags the emplace_back() into the
hash stack under C++26: the constructor cannot throw, since cbegin() is
noexcept, but it did not say so. dump_frame's constructor is noexcept
for the same reason.

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

* Share one recursion depth limit, and copy the hash frame out of the stack

dump() and hash() each defined their own limit on how many nesting levels
they recurse into, and the operations still to come would have added more,
free to diverge over time. They now all use detail::recursion_depth_limit(),
in a header of its own; serializer::dump_depth_limit() and
hash_depth_limit() are gone.

hash_iteratively() now copies the frame it works on out of the stack and
changes the frame only through stack.back(), so nothing can refer into
the stack after entering an element has grown it.

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

* Parenthesize multiplications in the hash test for clang-tidy

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-24 17:12:02 +02:00
Niels Lohmann 305ca7dadd Add missing headers to BUILD.bazel and check it in CI (#5554)
* Add missing headers to BUILD.bazel and make its generator reproduce it

The "json" cc_library did not list three headers that the library
includes:

- detail/meta/logic.hpp (added in #5016, included by from_json.hpp)
- detail/input/number_parse.hpp (added in #5283, included by lexer.hpp)
- detail/input/string_scan.hpp (added in #5283, included by lexer.hpp
  and serializer.hpp)

Bazel's sandbox only exposes declared headers, so any target depending
on @nlohmann_json//:json and including <nlohmann/json.hpp> failed with
"'nlohmann/detail/meta/logic.hpp' file not found".

The file could not simply be regenerated, because the generator behind
"make BUILD.bazel" was stale: it wrote only the "json" cc_library and
dropped the load() statements, the license block, and the
"singleheader-json" target that were added by hand in #4584. The
generator now emits the complete file, so its output differs from the
previous BUILD.bazel only by the three headers. It also resolves the
glob against the project root instead of the working directory and
sorts the list explicitly.

"make BUILD.bazel" is now phony: in a fresh checkout, BUILD.bazel is
not older than the headers, so make considered it up to date, and a
removed header would never trigger a rebuild. "make check-amalgamation"
also checks that BUILD.bazel is up to date.

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

* Check in CI that BUILD.bazel is up to date

The "Check amalgamation" workflow now also regenerates BUILD.bazel, so a
pull request that adds, renames, or removes a header without updating
the Bazel header list fails, and the attached amalgamation.patch
contains the fix. The failure comment and the contribution guidelines
mention the new check, and the comment now links to the existing
"Amalgamate the source code" section instead of the "Files to change"
anchor that was removed in #4560.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-24 17:07:21 +02:00
Niels Lohmann 2e91641de2 Test JSON_BRACE_INIT_COPY_SEMANTICS for real, and fix one-element tuples under it (#5544)
* Test JSON_BRACE_INIT_COPY_SEMANTICS for real, and fix one-element tuples under it

The opt-in JSON_BRACE_INIT_COPY_SEMANTICS was never exercised by CI:

- Its only test, in unit-regression3.cpp, was guarded by
  `#if defined(JSON_BRACE_INIT_COPY_SEMANTICS)` after the #include. The
  header #undefs the macro unconditionally in macro_unscope.hpp, so the
  guard was always false and the test compiled to nothing, whatever -D
  flag was passed.
- The ci_test_brace_init_copy_semantics target that passes the flag was
  not named by any workflow.

Move the test into its own translation unit that defines the macro before
including the header, as unit-diagnostics.cpp does for JSON_DIAGNOSTICS.
It now runs in every CI job and for every standard. Remove the unused
target: it ran the whole suite with the macro, and that suite deliberately
relies on default brace-init semantics in about 90 places
(e.g. `json({1})` meaning `[1]`), so it could never pass.

Running the whole suite with the macro did find one library bug:
to_json for std::tuple builds `j = { std::get<Idx>(t)... }`, so with copy
semantics a one-element tuple became its element. `json(std::tuple<int>{5})`
was `5` instead of `[5]`, and `get<std::tuple<int>>()` threw type_error.302
on the result. Under the macro, a one-element tuple now builds exactly what
the default deduction builds. Without the macro nothing changes.

The new tests also pin that the library's other conversions produce the
same values with and without the macro. The macro page now says that the
macro affects every single-element list (`json j = {1}` is `1`), and that
all translation units must agree on it, since it has no ABI tag.

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

* Make JSON_BRACE_INIT_COPY_SEMANTICS part of the ABI tag

The macro changes the body of the initializer-list constructor and adds a
to_json_tuple_impl overload, both with the same mangled names in either
mode, so mixing translation units silently picked one definition. Encode
it in the inline namespace as `_bics`, as JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
does with `_ldvcmp`. The macro is new in the unreleased 3.13.0, so no
existing namespace name changes.

- Move the macro's default into abi_macros.hpp so json_fwd.hpp computes
  the same namespace, and keep it defined under JSON_TEST_KEEP_MACROS.
- Check the tag in the ABI config tests and in the unit test.
- List `_bics` (and the missing `_dp`) in the namespace docs and in the
  natvis generator; regenerate nlohmann_json.natvis.
- Replace the "define it consistently" warning with an ABI note.

Suggested by @gregmarr in the review of #5544.

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

* Fix the cppcheck, clang-tidy and legacy-comparison CI failures

- to_json_tuple_impl() moved the element in both branches of a ternary;
  only one runs, but cppcheck reported accessMoved. Use if/else.
- The ABI tag test looked for "json_abi_bics", which misses when another
  tag comes first, as in json_abi_ldvcmp_bics; look for "_bics".
- readability-qualified-auto in the items() test.

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

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-24 17:05:37 +02:00
Niels Lohmann 8699de3064 Stop allocating the BJData excluded-marker list per container (#5555)
write_ubjson() built a std::vector of the eight markers BJData forbids as
the type of an optimized container - one heap allocation plus a linear
search for every array and object it wrote with use_type, even for plain
UBJSON output, where the list isn't consulted. The list was also spelled
out twice. A constexpr helper, is_bjdata_excluded_type_marker(), replaces
both.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-24 17:04:44 +02:00
Niels Lohmann 5f659c881a Let the labeler assign "aspect: binary formats", "python" and more "CI" (#5557)
- "aspect: binary formats" for changes to the binary reader or writer,
  their tests, fuzzers and docs, or with a binary format in the title;
- "python" for Python sources and pip requirements files, matching the
  label Dependabot sets on its pip updates, so it is never removed there;
- "CI" also for changes to the Dependabot and labeler configurations.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-24 17:03:48 +02:00
Niels Lohmann 36c079149a Refuse to build the fuzzer drivers with NDEBUG (#5562)
The fuzzer drivers check their round trips with assert(), which NDEBUG
compiles away. The OSS-Fuzz build keeps assertions on today, but nothing
pins that: a build change that adds NDEBUG would silently turn every
round-trip check into a mere "does not crash" check. Each driver now
stops the build with an #error instead, and includes <cassert> itself
rather than relying on json.hpp.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
2026-09-24 17:02:57 +02:00