extract_api.py's own extraction wasn't deterministic across machines, which CI's drift check caught immediately: JSON_HAS_RANGES auto-detects via the standard library's __cpp_lib_ranges feature-test macro, which isn't reliably gated to C++20 mode by every stdlib -- undefined under -std=c++17 with macOS's libc++, but defined under the identical flag with the Ubuntu stdlib CI uses, so parse()/accept()/from_*() extracted different signatures purely depending on which machine ran the extraction. Pinned to -DJSON_HAS_RANGES=0: the deterministic and safe choice, since pinning to 1 was tried first and found to fail to parse on a stdlib without full <ranges> support even when the macro claims otherwise. Also found and fixed a second, independent source of the same class of drift: get_identity_name() used cursor.spelling verbatim for CONVERSION_FUNCTION cursors, which libclang renders as its own internally-canonicalized form of the return type rather than what's literally written. Confirmed for json_pointer::operator string_t() spelling differently on two machines pinned to the identical libclang==18.1.1 wheel, with the JSON_HAS_RANGES fix above ruled out as the cause. Now derived from the cursor's own raw source text instead, immune to libclang's dependent-type resolution differences and incidentally more readable than the libclang-internal forms it replaces. Bumped SURFACE_FORMAT_VERSION to 3 and regenerated all 27 history snapshots and the committed api_surface.json; both fixes are documented in tools/api_checker/history/README.md's format-history log. Also fixes diff_api.py's format_version guard, which only compared the two loaded surfaces against each other and never against SURFACE_FORMAT_VERSION (what this build actually understands) -- two surfaces on the same, newer-than-expected format_version would have silently passed the guard. Remaining fixes are the concretely actionable findings from Codacy's review of the new tools/api_checker/ files: unused imports/variables, a stray f-string with no placeholders. Left the docstring-formatting nitpicks (pydocstyle D2xx/D4xx) and generic subprocess-usage notices alone -- the former has no established convention elsewhere in this codebase's Python tooling to conform to, and the latter are inherent to a dev tool that shells out to git/clang with developer-controlled arguments, not user input. Signed-off-by: Niels Lohmann <mail@nlohmann.me> Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Signed-off-by: Niels Lohmann <mail@nlohmann.me>
API surface history
One file per released v3.* tag: <tag>.json, e.g. v3.12.0.json. Each is the output of
extract_api.py --surface-output at that tag (see tools/api_checker/README.md for the schema),
with additional immutable provenance in meta: ref, commit (the tag's resolved commit sha),
generated_at, and generator.
Conventions
- Immutable once committed. Files here are never hand-edited or silently regenerated.
snapshot_release.pyrefuses to overwrite an existing file unless--forceis passed, and that should only happen for a deliberate, reviewed fix — the resulting diff should be inspected before committing, same as any other source change. - Generated by
tools/api_checker/snapshot_release.py, run manually as part of cutting a release (seetools/api_checker/README.md's "Workflow: Release Checklist"). Not CI-automated. diff_api.pyuses these automatically.--old/--newcheck here first (matching the ref string to<ref>.json) before falling back to livegit archiveextraction — seetools/api_checker/README.md'sdiff_api.pysection.
Coverage
Backfilled: every v3.* tag from v3.1.0 through the latest release at backfill time
(v3.12.0), covering the full public API history of the include/nlohmann/ header layout.
Format history
-
format_version: 2(current). Fixedextract_api.py'sABI_TAG_PATTERNto also strip the pre-rename ABI inline-namespace form used byv3.11.0/v3.11.1:json_v3_11_0(no_abisegment), renamed to today'sjson_abi_v3_11_2-style tag starting withv3.11.2. Underformat_version: 1, that older form wasn't recognized, so everyscope/signatureatv3.11.0/v3.11.1retained the raw un-stripped namespace segment, makingdiff_api.pyreport essentially the entire API (~330 of ~332 entries) as removed-and-readded acrossv3.10.5->v3.11.0->v3.11.1->v3.11.2— a bug of the same shape as the earlier USR-arity issue documented inextract_api.py'sidentity_key()docstring, caught the same way: by actually diffing real consecutive release pairs instead of trusting the extractor in isolation. All 27 files were regenerated underformat_version: 2; onlyv3.11.0.jsonandv3.11.1.jsonactually changed content (every other tag's scopes never contained the old-style tag). -
format_version: 3. Found via a CI run on a different machine than the one that generatedformat_version: 2, which is exactly the failure class this versioning exists to catch:JSON_HAS_RANGESauto-detects via the standard library's__cpp_lib_rangesfeature-test macro. That macro isn't reliably gated to C++20 mode by every stdlib -- undefined under-std=c++17with macOS's libc++, but defined under the identical flag with the Ubuntu stdlib used in CI -- soparse()/accept()/from_bjdata()/etc. extracted a different signature (with or without aSentinelTypeparameter) purely depending on which machine ran the extraction. Now pinned to-DJSON_HAS_RANGES=0inextract_api.py's parse arguments: the deterministic choice, since forcing1was tried first and found to actually fail to parse on a stdlib without full<ranges>support even when the macro claims otherwise.get_identity_name()usedcursor.spellingverbatim forCONVERSION_FUNCTIONcursors, which libclang renders as its own internally-canonicalized form of the return type rather than what's literally written. Confirmed forjson_pointer::operator string_t(): spelled"operator nlohmann::json_pointer::string_t_helper<type-parameter-0-0>::type"on one machine and"operator typename string_t_helper<type-parameter-0-0>::type"on another, with both machines pinned to the identicallibclang==18.1.1wheel and the JSON_HAS_RANGES fix above ruled out as the cause. Now derived from the cursor's own raw source text instead (regex overget_signature_text()'s output), immune to libclang's dependent-type resolution differences and incidentally more readable ("operator string_t"/"operator ValueType"instead of the libclang-internal forms). All 27 files were regenerated underformat_version: 3.
Known gaps
v3.0.0,v3.0.1: not backfilled. These predate theinclude/nlohmann/directory structure entirely — headers lived undersrc/at that point (a singlesrc/json.hpp). Sinceextract_api.pyhardcodesinclude/nlohmann/json.hppas the entry point,snapshot_release.py --all-tagsfails cleanly on these two refs (git archive ... -- includefinds nothing) rather than silently producing a wrong/empty result. Not pursued: two tags, immediately superseded byv3.1.0, and supporting the pre-restructuring layout would need a separate header/include-path convention with no other benefit. If full pre-3.1 coverage is ever wanted,extract_api.pywould need asrc/json.hpp-aware mode first.- Pre-
v3.0.0tags (v1.x,v2.x,v3.0.0-rc*, etc.) were never attempted — out of scope for this backfill; seetools/api_checker/POLICY.mdfor the stated boundary.