Skip to content

Recover from parse errors when parse_error() returns true - #5601

Draft
nlohmann wants to merge 31 commits into
developfrom
claude/fix-issue-3989-db7e45
Draft

nlohmann wants to merge 31 commits into
developfrom
claude/fix-issue-3989-db7e45

Conversation

@nlohmann

@nlohmann nlohmann commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Addresses #3989 and supersedes #4522.

Problem

The return value of json_sax::parse_error() was documented both as "must return false" (parse_error) and as "whether the parsing should continue" (parse exceptions). The code just passed the value on (return sax->parse_error(...)), so a handler returning true got broken behavior:

  • JSON text: parsing stopped anyway, but sax_parse() returned true for invalid input such as [1,2,3,].
  • Binary formats: the readers kept reading after the error. CBOR 0x9F (an indefinite-length array) followed by the end of the input looped forever, calling parse_error() each time. A truncated MessagePack array32 made about 4 billion calls.

#4522 let the parser carry on after the error without repairing anything, which produced meaningless events (e.g. key(1) for {1}).

Change

parse_error() returning false stops parsing, as before. Returning true now recovers from the error.

JSON text: repair and continue

Every error is repaired with the smallest local edit:

Mistake Repair Example Result
missing , or : inserted [1 2], {"a" 1} [1,2], {"a":1}
missing value null for an object key or between commas in an array {"a":}, [1,,2] {"a":null}, [1,null,2]
trailing comma removed [1,2,] [1,2]
broken string an unknown escape stands for the character (as in JavaScript); an invalid \u escape, a lone surrogate, or ill-formed UTF-8 becomes U+FFFD ["a\qb"] ["aqb"]
broken number the longest valid beginning is kept [1., 2e+] [1,2]
unreadable value null [1, NaN, tru] [1,null,null]
number too large passed as infinity, together with its text [1e999] number_float(inf, "1e999")
stray : removed ["a":1] ["a",1]
member without a key skipped up to the next , or } {1:2, "b":3} {"b":3}
wrong closing bracket closes the innermost array or object {"a":[1,2}, "b":3} {"a":[1,2],"b":3}
input ends too early all open arrays and objects are closed {"a":[1,2 {"a":[1,2]}
text before the value skipped )]}'{"a":1} {"a":1}

Guarantees while recovering:

  • The SAX events stay balanced, and every key() is followed by exactly one value. A DOM-building handler therefore always gets a complete value.
  • Parsing always ends.
  • Each token is reported at most once, and the handler can stop at any error by returning false.
  • The first error's message and position are exactly those reported without recovery.

Binary formats: repair what is complete, complete what was read

The binary formats have no delimiters to find the next value by, so what can be repaired depends on whether the end of the item with the error is known. RFC 8949, Section 5.3 makes the same distinction for CBOR, and names the two choices a decoder has for an item that is complete but invalid: replace it with an error marker and continue, or stop. That is parse_error() returning true or false.

The item is complete, but cannot be passed on as it is: it is replaced, and reading continues after it.

Mistake Formats Repair
tag (with the default cbor_tag_handler_t::error) CBOR ignored
simple value other than false, true, null, e.g. undefined CBOR null
number too large for a custom number_float_t, like float all infinity
char (C) that is not ASCII BJData, UBJSON U+FFFD
invalid high-precision number (H) BJData, UBJSON longest valid beginning, as in JSON text, or null
high-precision number too large BJData, UBJSON infinity, with its text
object key that is not a string BON8, CBOR, MessagePack member skipped, like a member without a key in JSON text
element of a type the library does not read (ObjectId, datetime, decimal128, regex, ...) BSON null
string without its terminator; document whose size does not match BSON kept

The end of the item is unknown (the input ended, a byte is not a valid type marker, or a size cannot be right):

  • Reading stops, and the value read so far is completed: null for a key that waits for its value, and end events for all open containers.
  • BSON is the exception, because every document stores its size. The element gets null, the rest of its document is skipped, and reading continues after it.

sax_parse() result

sax_parse() now returns true only if the input was parsed without errors and no event returned false.

Comparison with VS Code's jsonc-parser

The design is modeled on the error-tolerant parser in microsoft/node-jsonc-parser (source checked):

  • Taken from it: the lenient string and number handling.
  • Differs, deliberately:
    • A missing : is inserted; jsonc-parser drops the member.
    • Missing values become null instead of silently disappearing, which would shift array indices and leave keys without values.
    • Brackets are matched; jsonc-parser has none, so {"a":[1,2}, "b":3} gives {"a":[1,2,"b"]} there.
    • Garbage before the top-level value is skipped; jsonc-parser gives up.
    • Every mistake is reported once, and the handler can stop at any error; jsonc-parser's onError returns void.

Implementation notes

  • Parser: sax_parse_internal takes a bool AllowRecovery. parse() and accept() use false, and their report_error returns std::false_type, so all recovery code is dead there even when the call isn't inlined. A parse-only program's __text is 260 bytes smaller than before, and none of the recovery functions are in it.
  • Lexer: two new functions, recover_token() (keeps the readable part of a rejected token) and unget_token() (lets a repair hand , ] } or the end of input back to the state evaluation). The hot path of scan_string is unchanged; three surrogate error paths now record how to resume.
  • Binary reader: binary_reader gets a bool AllowRecovery template parameter, too.
    • The public sax_parse uses true; from_*() use false, and their repair code is dead through the same std::false_type overloads as in the parser.
    • Each of the 41 error sites calls one of three functions:
      • report_error: the end of the item is unknown, so it stops.
      • report_repairable_error: the item is complete.
      • report_error_repairable_if: it depends on the byte, and the error is still reported in one place when recovery is not compiled.
    • The value read before a final error is completed by the reader itself from its container_stack, as the JSON parser does from its states. A key waiting for its value is noted where reading a value fails; the object and array of a BJData ndarray are tracked by a small counter.
    • skip_items skips a member whose key is not a string without passing events. It uses a heap stack of counts and a head reader per format for CBOR, MessagePack, and BON8.
    • replace_invalid_utf8 (in string_utils.hpp) substitutes maximal subparts (Unicode, Section 3.9).
  • DOM parsers: json_sax_dom_parser / json_sax_dom_callback_parser get a default InputAdapterType. The documented json_sax_dom_parser<json> has not compiled since json start/end position implementation #4517. json_sax_dom_parser is also exported from the C++ module, so that the documented way to recover into a json value works.

Performance

Median of 61 runs, before → after. Measured on an array of 40 000 small objects ("objects") and an array of 20 000 nested numeric arrays ("arrays"). The sax_parse handler is a do-nothing virtual counter, so the numbers show the parser loop itself.

clang (Apple Silicon, native) GCC 14 (x86-64, emulated)
parse (objects) 31.8 → 31.9 ms 510 → 509 ms
parse (arrays) 14.5 → 14.5 ms 185 → 185 ms
accept (objects) 12.6 → 12.5 ms 166 → 167 ms
sax_parse (objects) 13.1 → 13.5 ms 171 → 162 ms
sax_parse (arrays) 4.6 → 4.8 ms 63 → 53 ms

parse() and accept() are unchanged. sax_parse() with a handler that does nothing moves by a few percent, slower with clang and faster with GCC, so that part is code layout.

Binary formats, clang (Apple Silicon), median of 3×41 runs, develop → this PR, on the same 20 000 objects:

CBOR MessagePack UBJSON BSON BON8
from_*() 16.39 → 16.22 ms 15.48 → 15.34 ms 19.26 → 19.21 ms 20.44 → 20.09 ms 17.84 → 17.96 ms
sax_parse 4.91 → 4.89 ms 4.29 → 4.38 ms 5.67 → 5.81 ms 6.73 → 6.67 ms 5.35 → 5.17 ms

A program that only calls from_*() for all six formats is 136 bytes (0.14%) larger in __text.

In unoptimized builds, parse(), accept(), and from_*() do not instantiate the recovery code: the stubs that stand in for it are always inlined, and the parser reaches its recovery helpers only through the recovering instantiation.

Tests

  • unit-class_parser.cpp:
    • about 80 repair cases, plus comments, ignore_trailing_commas, strict and non-strict parsing, and NUL bytes;
    • event-level checks and error messages;
    • stopping in the middle of a repair;
    • the same inputs through contiguous, iterator, and stream input adapters;
    • 100 000-error inputs, to guard against quadratic behavior;
    • a sweep over every truncation, deletion, and insertion of []{},:"x\ NUL 0xFF in several documents, checking that events are balanced, error counts are bounded, and valid input is unaffected.
  • unit-sax_parse.cpp (new): every sax_parse call instantiates the parser and binary reader that recover from errors, which pushed unit-regression2.cpp and unit-regression3.cpp past the MinGW linker's section limit. So the tests that call sax_parse (these, and sax_parse cannot read CBOR that contains tags, including binary values with a subtype written by to_cbor #5676's tagged-CBOR test from unit-regression3.cpp) have a file of their own, and CONTRIBUTING.md points there.
    • Every binary format truncated at every length, plus a trailing byte, must give exactly one error and a well-formed prefix. The handler stops after 100 errors, so the old CBOR hang now shows up as a failure instead of a timeout.
    • A table of the repairs above, numbers out of range, and errors that stop.
    • A sweep over eight encodings: every byte is replaced by ten values or removed. Events must be balanced, errors bounded, an error reported exactly when from_*() fails, and the first error must be the one from_*() throws.
  • Fuzzers: all seven run a recovering, event-checking handler (fuzzer-recovering_checker.hpp) from memory and from a stream. The binary ones also check that it reports an error exactly when from_*() fails. Locally, with ASan/UBSan: 3.6 + 1.4 million runs for JSON text and 21 million for the binary formats found nothing.
  • Configurations run locally: C++11/C++20, ASan/UBSan, JSON_STRICT_NUL_HANDLING=1, JSON_DIAGNOSTICS=1, JSON_DIAGNOSTIC_POSITIONS=1; the CI's clang -Weverything and GCC 16 warning flags; the binary format suites (with stubbed test data); make check-amalgamation; docs example output and style check.

Docs

  • New page features/parsing/error_recovery.md with the rules, binary behavior, limitations, and an example.
  • Updated parse_error, sax_parse, the SAX interface and parse-exceptions pages, and the README.

Breaking changes

  • No signatures change.
  • Behavior changes only for SAX handlers whose parse_error() returns true, which was documented as "must return false":
    • JSON text is now repaired instead of parsing stopping.
    • The binary readers now repair complete items and read on. Where the end of an item is unknown, they stop instead of reading past the error, and complete the value read so far.
    • sax_parse() returns false after any error; before, it could return true for invalid input.
  • For all other handlers, and for parse(), accept(), and from_*(), events, error messages, and positions are unchanged.
  • Additive only:
    • a default template argument for detail::json_sax_dom_parser and detail::json_sax_dom_callback_parser;
    • a defaulted bool AllowRecovery template parameter for detail::binary_reader;
    • the module export of json_sax_dom_parser.

🤖 Generated with Claude Code

The return value of json_sax::parse_error() was documented both as "must
return false" and as "whether the parsing should continue", and the code
just passed it on. For JSON text, parsing stopped anyway, but sax_parse()
could report success for invalid input. The binary readers read on after
the error, looping forever on a CBOR indefinite-length array without its
end.

Now false stops parsing, and true recovers from the error:

- JSON text is repaired with the smallest local edit (insert a missing
  ',' or ':', remove a stray token, keep the readable part of a broken
  string or number, null for a value that cannot be read, close the
  innermost container at a wrong closing bracket and all of them at the
  end of the input), and parsing continues. The SAX events stay balanced,
  every key is followed by exactly one value, and each token is reported
  at most once.
- The binary formats cannot resynchronize, so they stop, but complete
  the value read so far.

sax_parse() returns false after any error. parse(), accept(), and the
from_*() functions never recover and compile to the same code as before.

Supersedes #4522.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
@nlohmann
nlohmann marked this pull request as draft September 27, 2026 18:39
…989-db7e45

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
…ue (#3989)

When the SAX parser asks to recover, the binary readers now repair an
item whose end is known and read on after it, as RFC 8949, Section 5.3
describes for CBOR:

- CBOR: tags are ignored, and simple values other than false, true, and
  null become null (RFC 8949, Section 6.1); a negative integer below the
  range of number_integer_t becomes the nearest floating-point number.
- Strings that are not valid UTF-8 get U+FFFD for each ill-formed
  sequence, as in JSON text; so does a UBJSON/BJData char above 0x7F.
- UBJSON/BJData high-precision numbers keep their longest valid
  beginning (via the lexer's recover_token()), or become infinity.
- Members whose key is not a string are skipped (CBOR, MessagePack,
  BON8), like members without a key in JSON text.
- BSON elements of types the library does not read (ObjectId, datetime,
  decimal128, ...) become null; a string without its terminator and a
  document whose size does not match are kept.

Where the end of an item is unknown, reading stops as before, except
that BSON skips to the end of the document, whose size it knows.

The value read before such an error is now completed by the reader from
its container stack, as the JSON parser does, instead of by a proxy SAX
parser, which is removed. Like the parser, binary_reader gets an
AllowRecovery template parameter, so that from_*() compile without the
new code.

Tests: a table of repairs, numbers out of range, errors that stop, and
a sweep over changed and removed bytes of eight encodings that checks
balanced events and that the first error is the one from_*() reports.
All fuzzers now run a recovering checker; the binary ones also check
that it reports an error exactly when from_*() fails.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
recover_number() used back(), pop_back(), front(), and
find_first_of(const char*), which the minimal alt_string of
unit-alt-string.cpp does not provide. Since the binary readers recover
UBJSON/BJData high-precision numbers with it, from_ubjson() instantiated
it, too, and the test no longer compiled. It now uses only size(),
operator[], and resize(), and unit-alt-string.cpp recovers from errors
in JSON text, UBJSON, and CBOR.

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

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

# Conflicts:
#	include/nlohmann/detail/input/binary_reader.hpp
#	single_include/nlohmann/json.hpp
Comment thread include/nlohmann/detail/input/binary_reader.hpp Fixed
Moving the report of an unsupported BSON element type into
skip_unsupported_bson_element() moved its snprintf() call, which
Flawfinder then reported as a new CWE-134 finding. The two hexadecimal
digits are now computed directly; the message is unchanged.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
MSVC stringizes the arguments of doctest's CHECK() so that a raw string
literal becomes an ordinary one, and then reported the "\q" in the new
alt_string recovery test as warning C4129, an error with /WX. The input
is now a variable. The same pattern with "\u0000" in the parser's
recovery test is replaced by a JSON value built from a std::string.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
…to claude/fix-issue-3989-db7e45

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The U+FFFD constant was a global std::string, which the clang CI build
rejects with -Wglobal-constructors. It is now a function.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
ci_clang_tidy flagged nested conditional operators in the BON8 skip
code and in remove_incomplete_utf8_sequence()
(readability-avoid-nested-conditional-operator), and two branches with
the same body in recover_string() (bugprone-branch-clone). Use if
chains instead of the nested conditionals and merge the two branches
into one condition; the short-circuit order is unchanged.

ci_test_noexceptions aborted in the #3989 regression test: it compares
the first recovered error with the message of the exception that
from_cbor() and friends throw, and under JSON_NOEXCEPTION that call
aborts instead of throwing. Guard binary_error_message() and its uses
with #if !defined(JSON_NOEXCEPTION), as other tests in the suite do.

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

Copy link
Copy Markdown
Owner Author

Fixed the two red jobs of run 36536773620 in 792853d:

ci_static_analysis_clang (ci_clang_tidy): clang-tidy reported four errors in code added by this PR:

  • binary_reader.hpp:4540 and lexer.hpp:2522: nested conditional operators (readability-avoid-nested-conditional-operator), in the BON8 skip code and in remove_incomplete_utf8_sequence(). Replaced with if chains.
  • lexer.hpp:2704: two branches with the same body in recover_string() (bugprone-branch-clone). Merged them into one condition. The short-circuit order is unchanged, so remove_incomplete_utf8_sequence() still runs only for the ill-formed UTF-8 case.

ci_cmake_options (ci_test_noexceptions): test-regression2_* aborted (SIGABRT) in "binary formats repair items whose end is known". The #3989 test compares the first recovered error with the message of the exception that from_cbor() and the other readers throw. Under JSON_NOEXCEPTION that call aborts instead of throwing. I guarded binary_error_message() and the checks that use it with #if !defined(JSON_NOEXCEPTION), as other tests in the suite do. The recovery checks themselves (values, error counts, balanced events) still run in that configuration.

Verification: local clang-tidy on tests/abi/diag/diag_on.cpp reports exactly these four diagnostics before the change and none after it (and nothing new on the changed lines). unit-regression2 passes when built with -DJSON_NOEXCEPTION and run with --no-throw, like the CI target does. unit-regression2, unit-class_parser, unit-class_lexer, and unit-alt-string pass in a normal C++17 build. I regenerated the amalgamation with make amalgamate.

— posted by Claude Code on behalf of @nlohmann

clang-tidy's bugprone-derived-method-shadowing-base-method rejected the
test SAX parsers that derive from json_sax_dom_parser (or the test's
SaxEventLogger) and redefine their non-virtual event functions. The
recovering DOM parsers of unit-class_parser.cpp and unit-regression2.cpp
now hold a json_sax_dom_parser and forward to it, SaxEventLogger gets a
flag to recover from errors instead of a derived class, and the one
function unit-alt-string.cpp redefines is marked as intended.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann added a commit that referenced this pull request Sep 30, 2026
…arse()

json_sax_dom_callback_parser and json_sax_dom_parser branches of
parser::parse() ran the same ~25 lines after sax_parse_internal():
the strict-mode EOF check (raising parse_error.101 through the SAX
parser), release_lookahead() in non-strict mode, and mapping an
errored SAX parser to a discarded result. The two copies had already
drifted apart in formatting and in the second copy's "see above"
comment.

Add a private parse_dom(DomSax&, strict) member that runs this shared
sequence once and returns whether the SAX parser did not error; both
branches of parse() now only construct their DOM SAX parser, call
parse_dom(), and (for the callback parser) map a discarded top-level
value to null. sax_parse() is left untouched, since it only runs the
EOF check and release_lookahead() when sax_parse_internal() succeeded,
unlike parse(), which runs them unconditionally.

Behavior-preserving: same operations in the same order for both SAX
parser kinds. Verified with unit-class_parser (strict/non-strict,
callback and non-callback), unit-deserialization and
unit-disabled_exceptions (JSON_NOEXCEPTION), plus a clean
make amalgamate / make check-amalgamation diff.

Overlaps #5601, which touches the same lines.

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

#5712 item 2
nlohmann added a commit that referenced this pull request Sep 30, 2026
tests/src/round_trip_corpus.hpp exists so that the byte-stability
invariant the fuzzer drivers check also runs on a fixed corpus in CI,
instead of only at OSS-Fuzz. So far only the UBJSON and BJData drivers
had a matching unit test; the CBOR, MessagePack, BSON and BON8 drivers
assert the same invariant (assert(to_X(j2) == vec)) but nothing ran it
outside OSS-Fuzz.

Add "<FORMAT> round-trip invariants" test cases to unit-cbor.cpp,
unit-msgpack.cpp, unit-bson.cpp and unit-bon8.cpp, modeled on the
UBJSON case: seed j1 from the corpus (skipping values that do not
survive the format's own round trip, as the fuzzer drivers only ever
see values from_X() actually produced), then require from_X(to_X(j1))
not to throw and check to_X(j2) == to_X(j1). BSON only serializes
objects, so non-object corpus values are skipped. Update the comments
in round_trip_corpus.hpp and tests/fuzzing.md to name all six formats.

The stream-versus-contiguous check in the BON8 driver is left out, as
#5601 reworks it.

A local probe confirms no violations on the current corpus (CBOR 3849
checked, MessagePack 3909, BSON 2958, BON8 3841 - matching the counts
already recorded for this probe in the issue).

Closes #5714 item 1.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann added a commit that referenced this pull request Sep 30, 2026
The header comment of six of the seven binary-format fuzzer drivers
listed an invariant the code does not check: CBOR, MessagePack, BSON
and BON8 said "assert(j1 == j2)", but the code checks byte stability,
assert(to_X(j2) == vec). UBJSON and BJData still described the old
"assert(j1 == j2/j3/j4)" byte-exact check from before PR #5494 replaced
it with a use_size/use_type-aware round trip (UBJSON) and a
value-stability check (BJData); BJData's added paragraph already
explained the new check, but the step list above it did not.

Also remove a dead branch in fuzzer-parse_bson.cpp: from_bson() is
called with allow_exceptions = true, so it throws instead of returning
a discarded value, and the "if (j1.is_discarded()) return 0;" guard
could never trigger. Drop the unused <iostream> include from all seven
drivers and <sstream> from all but fuzzer-parse_bon8.cpp, which is the
only one that uses std::istringstream.

Overlaps #5601, which edits all seven drivers in the same hunks.

Closes #5714 item 4.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann added a commit that referenced this pull request Sep 30, 2026
parse_cbor_internal() hand-wrote the same "read a 1/2/4/8-byte
big-endian unsigned integer" ladder four times over:
- twice for tag numbers 0xD8-0xDB, once in the tag_handler::ignore
  branch and once, nearly identically, in the ::store branch (~90
  lines to read one integer);
- twice more for container lengths, once for array heads 0x98-0x9B and
  once for map heads 0xB8-0xBB, where the 1/2-byte forms called
  enter_array()/enter_object() directly and the 4/8-byte forms
  additionally went through get_cbor_container_size().

Add get_cbor_argument(std::uint64_t&), reading the width selected by
current & 0x1F via the same get_number() calls as before (so EOF is
reported exactly as before), and route all four sites through it:
- 0xD8-0xDB now read the argument once per branch instead of switching
  on `current` a second time; behavior split cleanly from embedded tags
  0xC0-0xD7 (tag value in the head, no argument to read), which is now
  its own case block that no longer has to fall into the ::store
  switch's "default" case to reach the same tag_pending = true; return
  true; outcome.
- 0x98-0x9B and 0xB8-0xBB collapse into one case block each, always
  going through get_cbor_container_size() (harmless for 1/2-byte
  lengths, which already always fit).

Verified byte-for-byte identical behavior before/after with a
standalone probe covering embedded and multi-byte tags under all three
tag_handler_t settings, a tag over a byte string (subtype path),
truncated tag/length arguments of every width, and array/map lengths
of every width, including the out_of_range.408 "excessive size" case:
same exceptions, same messages, same chars_read, same successful
results.

Left the string/byte-string length ladders in get_cbor_string()/
get_cbor_binary() untouched, as noted in #5711 item 2, since #5325 is
expected to touch them separately.

Overlaps #5601 (adds a branch right above the embedded-tag case) and
#5607 (touches the integer cases 0x18-0x1B, which share this ladder's
shape in separate hunks).

#5711 item 2

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann added a commit that referenced this pull request Sep 30, 2026
Every container is opened through enter_container(), whose docs
promise that a check placed there runs before every start event. The
close side had no equivalent: the same
"container_stack.pop_back(); dispatch to end_object() or end_array()"
sequence was written out separately in BSON, CBOR, MessagePack,
UBJSON/BJData and BON8, each copying the pattern of keeping an
is_object flag around the pop_back() that would otherwise invalidate
a reference to it. A check needed on close would have had to be added
in five places, and a sixth copy could go unnoticed.

Add leave_container() next to enter_container(), doing the same
pop-then-dispatch, and replace the five sites with it. Each site keeps
its own surrounding logic (BSON's check_bson_document_size() call
before popping, MessagePack's is_object copy used again below,
UBJSON/BJData's remaining-container handling after popping, BON8's
top used again below); only the repeated pop/dispatch line pair is
now shared.

Verified all six binary-format unit suites and unit-regression2's
deep-nesting tests (dependent count/reuse count and the bjdata ndarray
depth cases) still pass, compiled with -Wall -Wextra and ASan/UBSan.

Overlaps #5601, which is expected to add a sixth close site in its own
skip loop; that site can route through leave_container() too once it
lands.

#5711 item 4

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann added a commit that referenced this pull request Sep 30, 2026
…has it

binary_reader's constructor stores the format in the input_format
member, and sax_parse(format, sax_, strict, tag_handler) took the same
value again purely to dispatch on it. Every in-tree caller passed the
same value both times (all 16 from_cbor/from_msgpack/from_ubjson/
from_bjdata/from_bon8/from_bson call sites in json.hpp, and the three
public basic_json::sax_parse() overloads), so nothing was broken
today, but a caller of the detail class directly (only reachable via
JSON_PRIVATE_UNLESS_TESTED, as unit-bjdata.cpp already does) could
pass a mismatched pair - say bjdata to the constructor and ubjson to
sax_parse - and dispatch on one format while applying the other
format's rules; the default-constructed input_format_t::json reader
would additionally hit JSON_ASSERT(false) in exception_message() on
its first error.

Add sax_parse(json_sax_t*, bool, cbor_tag_handler_t) forwarding to the
existing overload with the stored input_format, and switch every
caller to it: the 16 from_*() sites (keeping their
`// cppcheck-suppress[accessMoved]` comments) and the three
basic_json::sax_parse() overloads, all of which already had the format
available from their own `format` parameter. The four-argument overload
is kept for anyone still calling it, now with
JSON_ASSERT(format == input_format) so a mismatch fails immediately
in a debug build (assert-enabled binaries, including the fuzzers and
test suite) instead of misbehaving; verified with a probe that
constructs a reader for one format and calls the explicit overload
with another, which aborts on that assertion as expected.

Removing or asserting against the constructor's input_format_t::json
default, which would affect direct detail users, is left as a separate
decision per #5711 item 5.

Overlaps #5601, which is expected to add an AllowRecovery template
parameter to sax_parse() and touch these same call sites in json.hpp.

#5711 item 5

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann added a commit that referenced this pull request Sep 30, 2026
…ecks

get_ubjson_size_value()'s 'i'/'I'/'l'/'L' cases each read a differently
sized signed integer and then repeated the same "reject negative with
error 113" check; only 'L' additionally checked value_in_range_of for
the out_of_range.408 case. Any change to that error path had to be
made four times.

Add get_ubjson_signed_count<SignedType>(std::size_t&), doing the read,
the negative check and the range check once, and route all four
markers through it. The range check is a no-op for 'i'/'I'/'l' (their
values always fit std::size_t) and only live for 'L' on a 32-bit
std::size_t target, matching today's behavior exactly.

In the ndarray dimension-product loop, the preceding loop already
returns early on any zero dimension and result starts at 1, so `i > 0`
in the pre-multiplication overflow check was always true, and
`result == 0` in the post-multiplication check could not be reached
either: two positive factors whose product does not overflow (as the
pre-check already guarantees) cannot be zero. Drop the dead `i > 0 &&`
and narrow the post-check to `result == npos`, the one case the
pre-check cannot rule out (an exact, non-overflowing match with the
sentinel reserved for unknown-size containers), with a comment
explaining why.

Verified byte-for-byte identical behavior before/after with a
standalone probe covering negative counts for every marker, a matching
positive count, and ndarray inputs, plus the full unit-ubjson and
unit-bjdata suites (same assertion counts as before this change).

Overlaps #5601 (rewrites the four parse_error calls and the overflow
checks touched here) and #5607/#5707 (touch neighboring lines in the
same functions).

#5711 item 6

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

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

# Conflicts:
#	tests/src/unit-alt-string.cpp
nlohmann added a commit that referenced this pull request Sep 30, 2026
* Fix stale and missing comments in binary_writer

The doc block of write_number() ended up above the byte_swap() helpers
added in #5286, about 80 lines from the function. It was also a plain
comment that Doxygen skips, said "write a number to output input", and
left BON8 out of the big-endian formats. Move it back onto
write_number() as a /*! block and fix the text.

write_bson() documented "@pre j.type() == value_t::object", but it
throws type_error.317 for every other type, and to_bson() relies on
that. Document the exception instead.

Explain why the CBOR binary subtype is always written with a 0xD8..0xDB
head and never in the one-byte tag form: binary_reader with
cbor_tag_handler_t::store only keeps those heads as a subtype, so
switching to write_cbor_head() would break round trips for subtypes
0..23.

Also fix the grammar of the to_char_type comment. Comments only; no
change in behavior, API or ABI.

Part of #5710

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

* Merge the duplicated UBJSON/BJData integer marker ladders

write_number_with_ubjson_prefix() (unsigned and signed overloads) and
ubjson_prefix() (number_integer and number_unsigned cases) each picked
the UBJSON/BJData integer marker (i, U, I, u, l, m, L, M, H) with their
own independent if/else ladder, and the values beyond 64 bits were
handled by a second, tag-dispatched pair of ladders. An optimized
container announces the marker of its first element via ubjson_prefix()
and then writes every element through write_number_with_ubjson_prefix(),
so the two had to be kept in lockstep by hand across four call sites.

Replace all of that with one ubjson_integer_prefix() built on
value_in_range_of<T>, and one write_ubjson_integer_payload() that
writes the value (or, for 'H', the decimal digits) for a given marker.
write_number_with_ubjson_prefix() and ubjson_prefix() keep their
signatures and now just call these two helpers.

Behavior, the public API and the ABI are unchanged. Verified with a
new regression test covering scalars and $-optimized arrays/objects at
every int8/uint8/int16/uint16/int32/uint32/int64/uint64 boundary for
to_ubjson/to_bjdata (both use_size/use_type settings), and by diffing
to_ubjson/to_bjdata output before and after over the json_test_data
corpus (bit-identical).

Part of #5710

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

* Remove dead get_char parameters in binary_reader

The non-recursive rewrite of the binary readers (#5505, #5506, #5507)
left parse_cbor_internal()'s and parse_ubjson_internal()'s get_char
parameters dead: parse_cbor_internal() has one caller and it always
passes true, and parse_ubjson_internal() has one caller and it always
uses the true default. Both parameters, and the @PARAM docs describing
the "reuse the last character" mode they used to select, no longer
correspond to anything.

Drop both parameters, initialise fetch/prefix unconditionally, and
update the two call sites in sax_parse(). parse_cbor_value()'s and
get_ubjson_string()'s own get_char parameters are unrelated and are
left alone; both still have a false caller.

Also delete a stray `@return whether a valid MessagePack value was
passed to the SAX parser` doxygen block that sits directly above
parse_msgpack_value()'s real doc comment, a leftover of the same
rewrite.

Behavior, the public API and the ABI are unchanged; these are private
members of detail::binary_reader. Verified by compiling with
-Wunused-parameter and running unit-cbor, unit-ubjson, unit-bjdata and
unit-msgpack (offline, against the stubbed test_data.hpp).

Part of #5711

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

* Share the IEEE half-precision decoder between CBOR and BJData

binary_reader had two ~45-line copies of the IEEE 754 half-precision
decoder: CBOR's case 0xF9 and BJData's case 'h'. Once formatting is
normalised, the two blocks were identical except for the byte order
used to assemble the 16-bit half (CBOR is big endian, BJData is little
endian). Any future change to half-float decoding had to be made and
kept in sync in both places.

Add one get_half_float(format, little_endian) helper that does the two
get()/unexpect_eof() reads, assembles the half in the requested byte
order, decodes it per RFC 8949 Appendix D, and calls sax->number_float.
Both cases now just call it with their byte order; the BJData case
keeps its bjdata-only guard.

Behavior, the public API and the ABI are unchanged. Verified with a
scratch probe comparing the old and new decoders bit-for-bit (NaN by
isnan()) over all 65536 wire byte pairs, in both formats, and by
running unit-cbor and unit-bjdata (offline, against the stubbed
test_data.hpp).

Part of #5711

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

* Deduplicate the MessagePack unsigned-integer writer ladder

The number_integer (non-negative branch) and number_unsigned cases in
write_msgpack() each held their own copy of the fixint/uint8/16/32/64
ladder, kept in lockstep only by a comment ("we used the code from the
value_t::number_unsigned case here"). Both copies mixed union members:
the signed copy compared number_unsigned but wrote number_integer, and
vice versa.

Extract write_msgpack_unsigned(std::uint64_t), mirroring how
write_cbor_head() already avoids the same duplication for CBOR, and
call it from both cases. Each case now reads only its own active
union member. Output bytes are unchanged for the default 64-bit
number types.

#5710 item 3

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

* Unify float marker selection and fix the long double compile error

Four formats picked between a float32 and float64 marker through four
different helper styles: dummy-argument overloads for CBOR and
MessagePack, an std::is_same template for BON8, and a runtime if-chain
on input_format_t for write_compact_float(). With number_float_t set
to long double, to_cbor, to_msgpack and to_ubjson failed inside the
library with "call to 'get_cbor_float_prefix' is ambiguous", while
to_bson kept working because write_bson_double() takes a plain double.

Change write_compact_float() to take the two marker bytes directly
(each of its three callers already knows them at compile time) instead
of an input_format_t it only forwarded, and delete the now-unused
get_cbor_float_prefix(), get_msgpack_float_prefix(),
get_bon8_float_prefix() and get_compact_float_prefix() helpers. Turn
the two get_ubjson_float_prefix() overloads into one template. Both
write_compact_float() and get_ubjson_float_prefix() now report an
unsupported number_float_t with a static_assert naming the requirement,
rather than an ambiguous-overload error; the assert lives in the
function body, not the class scope, so to_bson with long double is
unaffected.

Verified with a probe basic_json<..., long double>: to_bson still
compiles and round-trips, while to_cbor/to_msgpack/to_ubjson now fail
to compile with the new static_assert message.

This changes the text of an existing compile error for users with an
unsupported number_float_t (documented as a public-API-visible change
in #5710).

#5710 item 1

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

* Deduplicate the BJData ndarray writer's dtype dispatch and drop <map>

write_bjdata_ndarray() built a 12-entry std::map<string_t, CharType> on
every call just to translate the _ArrayType_ name to a dtype marker
(the only reason binary_writer.hpp included <map>), then mapped dtype
to C++ type twice more: once as a switch for the range-check pass and
once as a separate if/else chain for the write pass, with nothing
checking that the two agreed. The caller also ran three at() lookups,
and the callee called value.at(key) about ten more times for the same
three members.

Replace the map with bjdata_ndarray_type_marker(), a plain string
comparison chain (a C++11 constexpr function cannot contain a switch,
so this mirrors binary_reader's own static table style). Replace the
switch/if-chain pair with one write_bjdata_ndarray_elements() that
switches on dtype once and calls a per-type helper -
write_bjdata_ndarray_element<T>() for the eight integer dtypes and
write_bjdata_ndarray_float_element() for 'd' - with a dry_run flag
selecting the range check or the actual write, so the two passes can
no longer disagree on the type. _ArrayType_, _ArraySize_ and
_ArrayData_ are now looked up once into references, and the four
header marker bytes ('[', '$', '#') are written through to_char_type()
like the rest of the UBJSON/BJData writer.

The 'd' (single-precision) rule is left exactly as before, since #5707
is expected to change it separately.

Verified byte-for-byte identical output before/after for every dtype
(including the Draft 2/Draft 3 'byte' fallback and the use_count/
use_type combinations) via a standalone probe, plus round-tripping
through from_bjdata().

Overlaps #5707, which is expected to touch the 'd' dtype case, and
#5518, which is expected to move the write_bjdata_ndarray() call site.

#5710 item 4

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

* Assert that write_bson_document() consumes every calc_bson_sizes() entry

calc_bson_sizes() and write_bson_document() are a hand-synchronized
pair of passes over the same object/array tree, introduced by #5553:
the size pass appends to nested_sizes in visiting order, and the write
pass consumes the table by position with nested_sizes[next_size++].
Nothing checked that the write pass consumed the whole table. If a
future change touched only one of the two passes - for example to skip
or reject an entry - every later size prefix in the document would be
silently wrong.

Add JSON_ASSERT(next_size == nested_sizes.size()) where
write_bson_document() returns, so such a future drift between the two
passes is caught immediately (JSON_ASSERT expands to nothing in
release builds using assert(), and the fuzzers/tests already build
with it enabled). The two passes agree today, so this changes nothing
observable; it only guards against the risk described in #5710 item 5.

Extracting a shared stepper for the two passes (the second half of the
proposed change) is left for a follow-up: it only saves ~30 lines and
the issue asks for it only if the result reads clearly, which needs
more room to get right than a mechanical cleanup pass allows.

#5710 item 5

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

* Make the BJData lookup tables static functions instead of members

binary_reader held bjd_optimized_type_markers and bjd_types_map as
non-static const members (12 string_t objects for the type-name table),
built and destroyed on every from_cbor/from_msgpack/from_bson/
from_ubjson/from_bon8/from_bjdata call even though only from_bjdata
ever reads them. They also needed the #define/decltype/#undef
workaround from #3637 and two NOLINTNEXTLINE suppressions, and
binary_writer already carries the same two lists in another form
(is_bjdata_excluded_type_marker() and a local std::map in
write_bjdata_ndarray(), the latter removed by the item-4 commit), so
the excluded-marker lists could drift apart.

Replace bjd_optimized_type_markers with static constexpr
is_bjd_excluded_optimized_type(char_int_type), using the same ||-chain
as binary_writer's is_bjdata_excluded_type_marker(). Replace
bjd_types_map with a non-constexpr static bjd_type_name(char_int_type)
switch returning nullptr for an unknown marker (a C++11 constexpr
function cannot contain a switch). Delete both
JSON_BINARY_READER_MAKE_* macros, the bjd_type pair alias, the
NOLINTNEXTLINE suppressions, detail::make_array() (no longer used
anywhere), and the now-unused <algorithm> and <array> includes.

Update the two call sites (the ND-array excluded-type check and the
_ArrayType_ lookup) accordingly, and replace unit-bjdata.cpp's
"LUT arrays are sorted" section, which only checked the two tables'
internal ordering, with a check of all 12 type names and all 8
excluded markers against both new functions.

#5711 item 1

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

* Read CBOR's 1/2/4/8-byte argument through one helper

parse_cbor_internal() hand-wrote the same "read a 1/2/4/8-byte
big-endian unsigned integer" ladder four times over:
- twice for tag numbers 0xD8-0xDB, once in the tag_handler::ignore
  branch and once, nearly identically, in the ::store branch (~90
  lines to read one integer);
- twice more for container lengths, once for array heads 0x98-0x9B and
  once for map heads 0xB8-0xBB, where the 1/2-byte forms called
  enter_array()/enter_object() directly and the 4/8-byte forms
  additionally went through get_cbor_container_size().

Add get_cbor_argument(std::uint64_t&), reading the width selected by
current & 0x1F via the same get_number() calls as before (so EOF is
reported exactly as before), and route all four sites through it:
- 0xD8-0xDB now read the argument once per branch instead of switching
  on `current` a second time; behavior split cleanly from embedded tags
  0xC0-0xD7 (tag value in the head, no argument to read), which is now
  its own case block that no longer has to fall into the ::store
  switch's "default" case to reach the same tag_pending = true; return
  true; outcome.
- 0x98-0x9B and 0xB8-0xBB collapse into one case block each, always
  going through get_cbor_container_size() (harmless for 1/2-byte
  lengths, which already always fit).

Verified byte-for-byte identical behavior before/after with a
standalone probe covering embedded and multi-byte tags under all three
tag_handler_t settings, a tag over a byte string (subtype path),
truncated tag/length arguments of every width, and array/map lengths
of every width, including the out_of_range.408 "excessive size" case:
same exceptions, same messages, same chars_read, same successful
results.

Left the string/byte-string length ladders in get_cbor_string()/
get_cbor_binary() untouched, as noted in #5711 item 2, since #5325 is
expected to touch them separately.

Overlaps #5601 (adds a branch right above the embedded-tag case) and
#5607 (touches the integer cases 0x18-0x1B, which share this ladder's
shape in separate hunks).

#5711 item 2

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

* Add leave_container() to match enter_container()

Every container is opened through enter_container(), whose docs
promise that a check placed there runs before every start event. The
close side had no equivalent: the same
"container_stack.pop_back(); dispatch to end_object() or end_array()"
sequence was written out separately in BSON, CBOR, MessagePack,
UBJSON/BJData and BON8, each copying the pattern of keeping an
is_object flag around the pop_back() that would otherwise invalidate
a reference to it. A check needed on close would have had to be added
in five places, and a sixth copy could go unnoticed.

Add leave_container() next to enter_container(), doing the same
pop-then-dispatch, and replace the five sites with it. Each site keeps
its own surrounding logic (BSON's check_bson_document_size() call
before popping, MessagePack's is_object copy used again below,
UBJSON/BJData's remaining-container handling after popping, BON8's
top used again below); only the repeated pop/dispatch line pair is
now shared.

Verified all six binary-format unit suites and unit-regression2's
deep-nesting tests (dependent count/reuse count and the bjdata ndarray
depth cases) still pass, compiled with -Wall -Wextra and ASan/UBSan.

Overlaps #5601, which is expected to add a sixth close site in its own
skip loop; that site can route through leave_container() too once it
lands.

#5711 item 4

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

* Stop passing the input format to sax_parse() when the reader already has it

binary_reader's constructor stores the format in the input_format
member, and sax_parse(format, sax_, strict, tag_handler) took the same
value again purely to dispatch on it. Every in-tree caller passed the
same value both times (all 16 from_cbor/from_msgpack/from_ubjson/
from_bjdata/from_bon8/from_bson call sites in json.hpp, and the three
public basic_json::sax_parse() overloads), so nothing was broken
today, but a caller of the detail class directly (only reachable via
JSON_PRIVATE_UNLESS_TESTED, as unit-bjdata.cpp already does) could
pass a mismatched pair - say bjdata to the constructor and ubjson to
sax_parse - and dispatch on one format while applying the other
format's rules; the default-constructed input_format_t::json reader
would additionally hit JSON_ASSERT(false) in exception_message() on
its first error.

Add sax_parse(json_sax_t*, bool, cbor_tag_handler_t) forwarding to the
existing overload with the stored input_format, and switch every
caller to it: the 16 from_*() sites (keeping their
`// cppcheck-suppress[accessMoved]` comments) and the three
basic_json::sax_parse() overloads, all of which already had the format
available from their own `format` parameter. The four-argument overload
is kept for anyone still calling it, now with
JSON_ASSERT(format == input_format) so a mismatch fails immediately
in a debug build (assert-enabled binaries, including the fuzzers and
test suite) instead of misbehaving; verified with a probe that
constructs a reader for one format and calls the explicit overload
with another, which aborts on that assertion as expected.

Removing or asserting against the constructor's input_format_t::json
default, which would affect direct detail users, is left as a separate
decision per #5711 item 5.

Overlaps #5601, which is expected to add an AllowRecovery template
parameter to sax_parse() and touch these same call sites in json.hpp.

#5711 item 5

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

* Deduplicate UBJSON/BJData signed-count handling, drop dead ndarray checks

get_ubjson_size_value()'s 'i'/'I'/'l'/'L' cases each read a differently
sized signed integer and then repeated the same "reject negative with
error 113" check; only 'L' additionally checked value_in_range_of for
the out_of_range.408 case. Any change to that error path had to be
made four times.

Add get_ubjson_signed_count<SignedType>(std::size_t&), doing the read,
the negative check and the range check once, and route all four
markers through it. The range check is a no-op for 'i'/'I'/'l' (their
values always fit std::size_t) and only live for 'L' on a 32-bit
std::size_t target, matching today's behavior exactly.

In the ndarray dimension-product loop, the preceding loop already
returns early on any zero dimension and result starts at 1, so `i > 0`
in the pre-multiplication overflow check was always true, and
`result == 0` in the post-multiplication check could not be reached
either: two positive factors whose product does not overflow (as the
pre-check already guarantees) cannot be zero. Drop the dead `i > 0 &&`
and narrow the post-check to `result == npos`, the one case the
pre-check cannot rule out (an exact, non-overflowing match with the
sentinel reserved for unknown-size containers), with a comment
explaining why.

Verified byte-for-byte identical behavior before/after with a
standalone probe covering negative counts for every marker, a matching
positive count, and ndarray inputs, plus the full unit-ubjson and
unit-bjdata suites (same assertion counts as before this change).

Overlaps #5601 (rewrites the four parse_error calls and the overflow
checks touched here) and #5607/#5707 (touch neighboring lines in the
same functions).

#5711 item 6

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

* Drop redundant format parameter and dummy float argument (review)

binary_reader::sax_parse(format, ...) only ever had to equal the format
given to the constructor, which it asserted. With every caller already
on the format-less overload, remove the four-argument overload and
dispatch on the stored input_format directly. binary_reader is a
detail class, so this is not a public API change.

get_ubjson_float_prefix() took a value only to deduce its type; make
the type an explicit template argument instead.

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

---------

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

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

# Conflicts:
#	include/nlohmann/detail/input/binary_reader.hpp
#	include/nlohmann/json.hpp
#	single_include/nlohmann/json.hpp
nlohmann added a commit that referenced this pull request Oct 1, 2026
…5731)

* Share the diagnostic-position setter of the DOM SAX parsers

json_sax_dom_parser and json_sax_dom_callback_parser each had a private
copy of handle_diagnostic_positions_for_json_value(), identical except
for comments. Move the body into one static member function,
detail::diagnostic_positions::set_from_lexer(value, lexer), which both
classes call with their lexer pointer. basic_json befriends the new
struct (only when JSON_DIAGNOSTIC_POSITIONS is enabled), as the position
members are private.

The discarded case is reached through the callback parser, so the
LCOV_EXCL markers that only the dom parser's copy had are gone. The
NOLINT on the unreachable default case loses the stray
"-warnings-as-errors", which is not a check name.

The start-position setup in start_object()/start_array() is left alone,
as #5706 is editing the callback parser's versions.

Behavior, the public API and the ABI are unchanged.

Part of #5712

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

* Correct the parser comments on recursion and skip_to_state_evaluation

The class documentation called the parser a recursive descent parser,
but sax_parse_internal() is a loop that keeps the open containers on an
explicit stack. The comment at the end of an array and of an object
said the flag is set to false while the code below it sets it to true.
Describe what the code does instead.

Comments only; behavior, the public API and the ABI are unchanged.

Part of #5712

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

* Update the discard_number_values comments to the current number path

The comments explaining the accept() shortcut in convert_number() and
the member documentation still argued in terms of strtoull()/strtoll()
and errno, which #5283 replaced with convert_integer(), and pointed at
scan_number() instead of convert_number(). They also did not say that
scan_number_bulk_contiguous() converts integers itself, so the shortcut
is only reached for input without bulk access, with
JSON_DIAGNOSTIC_POSITIONS, or when the bulk scanner falls back.

Rewrite both comments to describe the digit-count check in front of
convert_integer(), keeping the 18-digit bound and the json_sax_acceptor
argument. The stale <cstdlib> comment is left for after #5616, which
edits that include block.

Comments only; behavior, the public API and the ABI are unchanged.

Part of #5712

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

* List the UTF-8 validators instead of calling the DFA the only one

The documentation of decode() called the Hoehrmann DFA the single
source of truth for UTF-8 validation. It is used only by the serializer
and by is_valid_utf8() (CBOR/MessagePack/BSON/UBJSON/BJData text
strings). The lexer's scan_string() switch, validate_one_utf8() /
valid_utf8_prefix() (bulk string scan, BON8 bulk path and BON8 writer)
and the BON8 byte path in get_bon8_string() check the RFC 3629 ranges
on their own.

Replace the sentence with a list of the four validators, what each is
used for, and a note that they must accept the same sequences. Sharing
code between them was considered and dropped: it would save a few lines
in a validator that is entangled with BON8 pushback, and #5677 is
editing the BON8 byte path.

Comments only; behavior, the public API and the ABI are unchanged.

Part of #5712

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

* Fix stale doc comments and include lists in the input headers

input_adapters.hpp included <memory> and <numeric> for the removed
shared_ptr-based adapter design but used neither; it called
(std::min) without including <algorithm>. json_sax.hpp used
std::numeric_limits without including <limits>. Also corrected
comments that no longer matched the code: input_stream_adapter does
not skip the input's BOM (the lexer's skip_bom() does), the
span_input_adapter comment named the no-longer-existing
input_buffer_adapter type, lexer::get_string() does not reset the
token, binary_reader's get_number() doc opened with /* instead of
/*! (so Doxygen skipped it) and omitted BON8 from its endianness
note, and the UBJSON-binary-types note did not mention that BJData
'B' arrays are read as binary.

Left out: the lgtm suppression on lexer.hpp's scan_number() (in
#5616's hunk) and the "-1 if unknown" wording in json_sax.hpp's
start_object/start_array docs (in draft #5267's hunk), per the
verdict's conflict list.

Part of #5712

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

* Deduplicate the strict-EOF/release_lookahead/error block in parser::parse()

json_sax_dom_callback_parser and json_sax_dom_parser branches of
parser::parse() ran the same ~25 lines after sax_parse_internal():
the strict-mode EOF check (raising parse_error.101 through the SAX
parser), release_lookahead() in non-strict mode, and mapping an
errored SAX parser to a discarded result. The two copies had already
drifted apart in formatting and in the second copy's "see above"
comment.

Add a private parse_dom(DomSax&, strict) member that runs this shared
sequence once and returns whether the SAX parser did not error; both
branches of parse() now only construct their DOM SAX parser, call
parse_dom(), and (for the callback parser) map a discarded top-level
value to null. sax_parse() is left untouched, since it only runs the
EOF check and release_lookahead() when sax_parse_internal() succeeded,
unlike parse(), which runs them unconditionally.

Behavior-preserving: same operations in the same order for both SAX
parser kinds. Verified with unit-class_parser (strict/non-strict,
callback and non-callback), unit-deserialization and
unit-disabled_exceptions (JSON_NOEXCEPTION), plus a clean
make amalgamate / make check-amalgamation diff.

Overlaps #5601, which touches the same lines.

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

#5712 item 2

* Share the code point to UTF-8 encoding between the wide-string helpers and the lexer

The 1/2/3/4-byte UTF-8 encoding ladder was written out by hand three
times: in wide_string_input_helper<..., 4>::fill_buffer() for a UTF-32
code point, in the UTF-16 helper for both a BMP code unit and a valid
surrogate pair, and in the lexer's \uXXXX/\uXXXX\uYYYY handling. The
copies had drifted: the UTF-32 helper masked the leading bits of each
byte (& 0x1Fu, & 0x0Fu, & 0x07u) where the others relied on the shift
alone, even though both give the same result for a code point that is
already known to be in range.

Add detail::encode_utf8(cp, out) in string_utils.hpp, a single encoder
that invokes a callable once per output byte, most significant byte
first. Use it in the three valid-code-point branches (UTF-32 code
points up to U+10FFFF, UTF-16 code units outside the surrogate range,
and valid UTF-16 surrogate pairs) and in the lexer's \u handling, where
out forwards to add(). The UTF-16 helper's deliberate pass-through of
malformed surrogate units and the UTF-32 helper's 0xFF sentinel for
code points above U+10FFFF are untouched, since neither reaches the new
helper.

Behavior-preserving: same bytes in the same order for every valid code
point, verified with unit-class_lexer, unit-class_parser,
unit-deserialization, unit-wstring and the non-test-data parts of
unit-unicode1..5 (ASan/UBSan, C++11/17/20), and an escape-heavy parse
microbenchmark that shows no change (about 73 ms either way, median of
3, 1M escape sequences). single_include/ regenerated with make
amalgamate; make check-amalgamation leaves a clean tree.

Overlaps #5704, which rewrites the wide_string_input_helper
specializations touched here.

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

#5712 item 6

---------

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
nlohmann added a commit that referenced this pull request Oct 1, 2026
…les (#5732)

* Run the README test case in JSON_FastTests jobs

The "README" test case was marked doctest::skip() when the tests
moved from Catch to doctest in 2019, where it replaced Catch's hidden
tag. It is not slow (17 assertions, about 0.00 s), but cmake/test.cmake
only passes --no-skip when JSON_FastTests is off, so the per-compiler
ci_test_*_cxxNN matrix, macOS, Windows Release/ARM, icpc, icpx and
nvhpc compiled the README examples without running them.

Drop the skip decorator so every job runs the case. Test-only change.

Part of #5713

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

* Remove stale clang ranges guards in unit-iterators2.cpp

The "algorithms" and "views" sections were guarded by clang/libstdc++
checks written for a clang 15 (04/2022) bug. The first guard's
condition contradicts its own comment: it skips clang+libc++ and
keeps clang+libstdc++. Both sections already sit inside
`#if JSON_HAS_RANGES`, which macro_scope.hpp excludes for the
toolchains these guards targeted, so the inner guards never let the
sections run on the platforms they meant to protect and are
redundant on the rest. Verified locally with Apple clang 21/libc++
and clang 16.0.6/libstdc++ 12 (Docker): both pass all 1355
assertions with the guards removed.

Part of #5713

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

* Fix copy-pasted CBOR half-float checks; enable stale encode checks

In the RFC 8949 Appendix A test case, the decode checks for
5.960464477539063e-8 (0xf9 0x00 0x01) and 0.00006103515625
(0xf9 0x04 0x00) were copy-pasted from the neighboring -4.0 example,
so those two half-float byte sequences were never actually decoded
and checked, and -4.0 was checked three times instead. The two
float32 encode checks for 100000.0 and 3.4028234663852886e+38 were
commented out before the writer supported emitting float32 and are
now verified to match byte for byte, so they are enabled. The
remaining commented-out half-precision to_cbor checks are collapsed
into a single explanatory comment, since the writer never emits
half-precision floats.

Part of #5713

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

* Assert on the result of STL container conversions in tests

The "object-like STL containers" and "array-like STL containers"
sections converted json values into std::map, unordered_map,
multimap, unordered_multimap, list, forward_list, array, valarray,
vector, deque, set and unordered_set and discarded the result, so
these ~60 conversions only proved that the code compiles and does
not throw; a conversion that dropped or reordered elements would
still pass. Bind each result and compare it against the expected
container. Also fix a copy-paste slip in the deque section
(`j2.get<std::deque<double>>()` instead of j3, so j3's doubles were
never converted to a deque), and remove the dead
`// CHECK(m5["one"] == "eins")` comments that referred to a variable
that did not exist by asserting the equivalent through the bound
result.

Part of #5713

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

* Deduplicate SaxCountdown and other test helpers across formats

SaxCountdown was copied byte-for-byte into six binary-format test
files (unit-cbor.cpp, unit-msgpack.cpp, unit-ubjson.cpp,
unit-bjdata.cpp, unit-bon8.cpp, unit-bson.cpp), about 370 redundant
lines. Move it into tests/src/sax_countdown.hpp (namespace utils,
alongside test_utils.hpp and round_trip_corpus.hpp) and include it
from all six.

trait_test_arg and the "value_in_range_of trait"
TEST_CASE_TEMPLATE_DEFINE were duplicated between unit-32bit.cpp and
unit-bjdata.cpp; the trait is a detail/meta trait, not specific to
either file. Move it into tests/src/value_in_range_of_test.hpp;
unit-32bit.cpp keeps its own include, since JSON_32bitTest=ONLY
builds only that file. Each file keeps its own
TEST_CASE_TEMPLATE_INVOKE list.

sax_no_exception and the "issue #2824" section were duplicated in
unit-regression2.cpp and unit-disabled_exceptions.cpp. Drop the copy
from unit-regression2.cpp; unit-disabled_exceptions.cpp already
covers the no-exceptions case that #2824 was about, and
ci_test_noexceptions reruns it.

No behavior change. Verified by building and running unit-cbor,
unit-msgpack, unit-ubjson, unit-bjdata, unit-bon8, unit-bson,
unit-32bit, unit-regression2 and unit-disabled_exceptions against
include/ (clang++ -std=c++11, ASan/UBSan where applicable); assertion
counts are unchanged from before the refactor.

Part of #5714

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

* Remove the unreferenced vendored libFuzzer

tests/thirdparty/Fuzzer (155 files, ~776 KB of vendored Apache-2.0
LLVM code from the 2016 OSS-Fuzz import) is not referenced by any
CMakeLists, Makefile or workflow: the fuzz drivers link against
-fsanitize=fuzzer or the repo's own
tests/src/fuzzer-driver_afl.cpp. Its vendored README only points at
llvm.org's own libFuzzer docs. Being dead code, it also adds noise
to the flawfinder code-scanning workflow, which scans the whole
tree. Remove the directory and its .reuse/dep5 entry.

Part of #5714

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

* Remove unreferenced 2016 benchmark and fuzz reports

tests/reports (1.6 MB) holds AFL status pages and plots from
2016-08-29 and 2016-10-02, and a nativejson-benchmark snapshot from
2016 with links to rawgit.com, which shut down in 2019. Nothing
references this directory: no doc, README section, script or
workflow points at it, and it describes a ten-years-old, pre-2.0
snapshot of the library.

Part of #5714

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

* Run the CBOR, MessagePack, BSON and BON8 round-trip invariants in CI

tests/src/round_trip_corpus.hpp exists so that the byte-stability
invariant the fuzzer drivers check also runs on a fixed corpus in CI,
instead of only at OSS-Fuzz. So far only the UBJSON and BJData drivers
had a matching unit test; the CBOR, MessagePack, BSON and BON8 drivers
assert the same invariant (assert(to_X(j2) == vec)) but nothing ran it
outside OSS-Fuzz.

Add "<FORMAT> round-trip invariants" test cases to unit-cbor.cpp,
unit-msgpack.cpp, unit-bson.cpp and unit-bon8.cpp, modeled on the
UBJSON case: seed j1 from the corpus (skipping values that do not
survive the format's own round trip, as the fuzzer drivers only ever
see values from_X() actually produced), then require from_X(to_X(j1))
not to throw and check to_X(j2) == to_X(j1). BSON only serializes
objects, so non-object corpus values are skipped. Update the comments
in round_trip_corpus.hpp and tests/fuzzing.md to name all six formats.

The stream-versus-contiguous check in the BON8 driver is left out, as
#5601 reworks it.

A local probe confirms no violations on the current corpus (CBOR 3849
checked, MessagePack 3909, BSON 2958, BON8 3841 - matching the counts
already recorded for this probe in the issue).

Closes #5714 item 1.

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

* Fix fuzzer driver step lists to match the checks the code performs

The header comment of six of the seven binary-format fuzzer drivers
listed an invariant the code does not check: CBOR, MessagePack, BSON
and BON8 said "assert(j1 == j2)", but the code checks byte stability,
assert(to_X(j2) == vec). UBJSON and BJData still described the old
"assert(j1 == j2/j3/j4)" byte-exact check from before PR #5494 replaced
it with a use_size/use_type-aware round trip (UBJSON) and a
value-stability check (BJData); BJData's added paragraph already
explained the new check, but the step list above it did not.

Also remove a dead branch in fuzzer-parse_bson.cpp: from_bson() is
called with allow_exceptions = true, so it throws instead of returning
a discarded value, and the "if (j1.is_discarded()) return 0;" guard
could never trigger. Drop the unused <iostream> include from all seven
drivers and <sstream> from all but fuzzer-parse_bon8.cpp, which is the
only one that uses std::istringstream.

Overlaps #5601, which edits all seven drivers in the same hunks.

Closes #5714 item 4.

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

* Silence the CMP0169 deprecation in cmake_fetch_content, fix stale guards

tests/cmake_fetch_content/project calls the single-argument
FetchContent_Populate(json) after FetchContent_Declare(), which CMake
3.30 deprecated as CMP0169. Since the project declares
cmake_minimum_required(VERSION 3.11...3.14), the policy stays unset,
so every configure with a current CMake prints the deprecation
warning. The test is kept on purpose: it is the only coverage of the
FetchContent_Populate + add_subdirectory pattern for CMake 3.11-3.13
users, which the docs still describe as supported. Explicitly set
CMP0169 to OLD, with a comment explaining why.

Also fix two stale version guards:
- tests/cmake_fetch_content/CMakeLists.txt guarded the test with
  VERSION_GREATER "3.11.0", which is dead now that tests/CMakeLists.txt
  requires CMake 3.13.
- tests/cmake_fetch_content2/CMakeLists.txt guarded with
  VERSION_GREATER "3.14.0", which skips exactly 3.14.0, the first
  version with FetchContent_MakeAvailable. Change it to
  VERSION_GREATER_EQUAL "3.14".

Verified locally: `ctest -R cmake_fetch_content` passes with CMake
4.1, and the CMP0169 deprecation warning that appeared before this
change is gone.

Closes #5714 item 5.

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

* Make the CMake integration-test wrappers consistent

The six tests/cmake_* integration-test wrappers had drifted:

- Only cmake_import and cmake_import_minver forwarded
  -A "${CMAKE_GENERATOR_PLATFORM}" to the inner configure, and none
  forwarded -T "${CMAKE_GENERATOR_TOOLSET}". The Windows workflow
  configures the outer build with -A Win32 -T ClangCL, so without
  forwarding, the inner projects of cmake_add_subdirectory,
  cmake_fetch_content, cmake_fetch_content2 and
  cmake_target_include_directories built with the generator defaults
  instead of matching the outer build's platform and toolset. Forward
  both consistently from all six wrappers.
- cmake_fetch_content and cmake_fetch_content2 passed
  -Dnlohmann_json_source to their inner projects, which never read it
  (CMake warns "manually-specified variables were not used"); the
  inner projects fetch their own copy of the library instead. Drop it.
- tests/CMakeLists.txt set JSON_FORCED_GLOBAL_COMPILE_OPTIONS from the
  matching environment variable but never read the cache variable
  again; the lines right below it read $ENV{JSON_FORCED_GLOBAL_COMPILE_OPTIONS}
  directly, like the LINK_OPTIONS counterpart already does. Remove the
  dead set().

This changes which platform and toolset the Win32 and ClangCL CI jobs
build the four newly-forwarding wrappers' inner projects with, which
may surface new failures there; CI has to confirm those jobs.

Verified locally with Ninja (empty -A ""/-T "" is accepted): all 12
cmake_* tests still pass, and the inner fetch_content configures no
longer warn about the unused variable.

Closes #5714 item 6.

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

* Turn the #972 fifo_map regression test into a real test

The #972 regression test in unit-regression1.cpp only built a
my_json array from a string literal (the original crash) and had no
CHECK, so the fifo_map object type it exists to demonstrate was never
exercised. Meanwhile the docs recommend fifo_map for keeping object
keys in insertion order (object_order.md, template_parameters.md),
and nothing tested that recommendation.

Extend the section: after the original array assignment, parse an
object with my_json::parse() (not via the "..."_json UDL, which
returns a plain nlohmann::json and would exercise the cross-basic_json
conversion constructor instead of the parser's own key insertion -
and, as tried locally, does not keep fifo order for this stateful
comparator) and check that dump() keeps insertion order, and that it
survives erase() and inserting a new key.

Also narrow thirdparty/fifo_map off the include path of every other
test-* target: it was a PUBLIC include directory of test_main, even
though unit-regression1.cpp is its only user. Add a small
fifo_map_include INTERFACE library with that include directory and
attach it to test-regression1 only via json_test_set_test_options().

Verified locally (test-regression1_cpp11, default build and
-fsanitize=address,undefined): the new checks pass; `git grep fifo_map
tests` still only finds unit-regression1.cpp and the vendored header.

Closes #5714 item 7.

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

* Document the vendored doctest.h patch; fix stale doctest_compatibility.h comments

tests/thirdparty/doctest/doctest.h is doctest 2.4.12, imported in
#4771. Two weeks later, #4801 hand-edited translateActiveException()
to declare "String res;" inside the translator loop instead of before
it, so a translator that does not match does not leave a previous
translator's result in "res" for the next iteration to see. Nothing
recorded this, so re-vendoring doctest.h from upstream would silently
drop the fix. Add a comment at the patched site naming the version,
the PR and the reason, so a future re-vendor knows to re-apply it.

Also fix two stale comments in doctest_compatibility.h:
- The DOCTEST_THREAD_LOCAL comment referenced Xcode 6/7, which is no
  longer supported; reword it to explain why the define must stay
  regardless (it keeps doctest's own thread_local usage out of the way
  of the same Clang/MinGW crash that JSON_NO_THREAD_LOCAL works around
  in the library, see ci_test_no_thread_local).
- The <iosfwd> include's comment justified it with tests that define
  "private" as "public"; no test under tests/src does that any more
  (removed by #2352). Reword the comment instead of dropping the
  include, since confirming it is safe to drop needs the full CI
  matrix including MSVC 2015+.

Verified locally that tests/src/unit-readme.cpp still builds and
passes 17/17 with these headers.

Closes #5714 item 9.

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

* Stop compiling unit-wstring.cpp out entirely on classic ICC

tests/src/unit-wstring.cpp wrapped the whole file in
#ifndef __INTEL_COMPILER, with the comment "ICPC errors out on
multibyte character sequences in source files". The ci_icpc job
(intel/oneapi-hpckit:2023.2.1) still exists, so that job ran none of
the wstring/u16string/u32string input adapter tests, including the
malformed-input checks #5704 (open) extends.

Only 9 lines contained non-ASCII bytes: the three *_is_utf16()/
*_is_utf32() probe functions, and three std::wstring/u16string/
u32string literals plus their narrow-string dump() expectations.
Rewrite all of them with \u/\U escapes in the wide/u16/u32 literals
and \x escapes (split into separate string-literal tokens so a
following byte is never read as part of the same hex escape, e.g.
"\xE1\x83\x85" "a") in the narrow ones. Remove the
#ifndef __INTEL_COMPILER/#endif guard along with it.

The *_is_utf16()/*_is_utf32() probes compared a raw multibyte literal
against an escape-based one to detect a compiler that misreads the
source file's encoding; with no raw literals left to misread, the
comparison is now tautological, so drop the probes and the "if"
guards around each SECTION's body instead of leaving them in as dead
checks.

The same non-ASCII-in-source-and-in-a-narrow-comparison pattern
existed once more in unit-deserialization.cpp's "Using _json with
char8_t literals #4945" test: a raw emoji character in a u8R"(...)"
literal, guarded by a check_utf8() that returned false for ICC (same
reason) and for Windows without the active UTF-8 code page. Rewrite
the literal with a \U escape and compare it against a \x-escaped
expectation instead of a second raw literal, and drop check_utf8()
and the now-unused <windows.h> include along with the guard.

Verified locally (clang, -std=c++11 and -std=c++20,
-fsanitize=address,undefined, and a plain build): test-wstring keeps
18/18 assertions and unit-deserialization keeps 466/466 (c++11) and
477/477 (c++20) assertions, matching this branch before the change
exactly - no coverage was gained or lost, only the source-encoding
dependency was removed. ci_icpc has to confirm classic ICC actually
builds and passes test-wstring now; if it does not, that is a real
finding, not a reason to restore the guard.

Overlaps #5704 (open), which edits unit-wstring.cpp inside the
previously-guarded region (an include near the top, checks in the
invalid-string sections, and a new section at the end).

Closes #5713 item 5.

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

---------

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

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

# Conflicts:
#	include/nlohmann/detail/input/lexer.hpp
#	include/nlohmann/detail/input/parser.hpp
#	single_include/nlohmann/json.hpp
#	tests/src/fuzzer-parse_json.cpp
…989-db7e45

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

# Conflicts:
#	docs/mkdocs/docs/api/json_sax/parse_error.md
#	docs/mkdocs/docs/features/parsing/index.md
clang's -Wdocumentation-unknown-command reads "\u" in a doc comment as
an unknown command, even in backticks. Two comments of the recovery code
now say "Unicode escape" instead.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The MinGW job ("clang (20.1.8)", Debug) failed to link test-msgpack_cpp17
with "relocation truncated to fit: IMAGE_REL_AMD64_REL32 against
`.rdata'". Its GNU ld (MinGW 12.2.0) cannot link an object file with
more than 32767 sections. Reproduced with clang 20.1.8 and binutils
2.40: unit-msgpack.cpp's object has 31958 sections on develop and had
33210 with #3989; binutils 2.44 links both.

The readers of from_*() never recover, but unoptimized builds still
emitted the recovery code their dead branches referenced: about 20
helpers per reader instantiation, plus the lexer's recovery functions
through the UBJSON high-precision number repair. Now:

- Errors are reported with report_error() only. The helpers that act on
  the SAX parser's answer (repair_requested(), resync(), value_failed(),
  close_open_containers(), and the repairs of high-precision numbers and
  unsupported BSON elements) have one trivial variant for readers that do
  not recover, so the recovery code is not referenced there.
- Object keys that are not strings and BSON elements whose end is lost
  are marked at the error site and skipped by resync() in the reading
  loops; report_repairable_error(), report_error_repairable_if(),
  report_bson_element_error(), and bon8_error_repairable_if() are gone.
- is_cbor_item_head() is a free function, and the BSON element type is
  formatted as on develop again.

The object now has 32638 sections and links. Behavior is unchanged
(regression tests, binary format suites, and fuzzing), and an optimized
program that only calls from_*() is now 256 bytes smaller than on
develop.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
#5737 enabled -Wextra-semi-stmt for the tests and removed the
semicolons after CAPTURE(), which expands to a complete statement; the
#3989 tests added twelve more.

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

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

# Conflicts:
#	tests/src/unit-alt-string.cpp
…989-db7e45

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

# Conflicts:
#	include/nlohmann/detail/input/binary_reader.hpp
#	include/nlohmann/detail/string_utils.hpp
#	single_include/nlohmann/json.hpp
Signed-off-by: Niels Lohmann <mail@nlohmann.me>
…989-db7e45

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

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

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

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

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

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

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The local alias that the out-of-range test declared shadows the identical
file-scope one, which GCC's -Wshadow=global turns into an error.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
The MinGW clang job links with a GNU ld that cannot relocate an object
with more than 32767 sections. unit-regression2, unit-regression3 and
unit-msgpack went over it:

- At -O0, the parser and binary reader for parse(), accept() and
  from_*() still emitted the stubs that stand in for recovering, and
  the parser referenced its recovery helpers from code that never runs.
  The stubs are now always inlined, and the parser reaches its helpers
  through the same allow_recovery tag as the stubs, so that this code
  is not even instantiated there. This removes about 300 sections from
  unit-msgpack.
- Every sax_parse call instantiates the parser and binary reader that
  recover from errors. The tests that call it move from
  unit-regression2.cpp (#3989) and unit-regression3.cpp (#5676) into
  unit-sax_parse.cpp, like unit-explicit_instantiation.cpp got a file
  of its own (#5511), and CONTRIBUTING.md points there.

Section counts of the MinGW Debug objects (clang 19, -g0), develop / PR:
unit-msgpack 32281 / 32558, unit-regression2 31272 / 31278,
unit-regression3 32392 / 27462, unit-sax_parse - / 19836.

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants