Files
json/tools/api_checker/history/README.md
T
Niels Lohmann 0663907b68 Fix cross-environment API-surface drift and lint findings from CI
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>
2026-07-11 18:59:19 +02:00

5.1 KiB

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.py refuses to overwrite an existing file unless --force is 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 (see tools/api_checker/README.md's "Workflow: Release Checklist"). Not CI-automated.
  • diff_api.py uses these automatically. --old/--new check here first (matching the ref string to <ref>.json) before falling back to live git archive extraction — see tools/api_checker/README.md's diff_api.py section.

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). Fixed extract_api.py's ABI_TAG_PATTERN to also strip the pre-rename ABI inline-namespace form used by v3.11.0/v3.11.1: json_v3_11_0 (no _abi segment), renamed to today's json_abi_v3_11_2-style tag starting with v3.11.2. Under format_version: 1, that older form wasn't recognized, so every scope/signature at v3.11.0/v3.11.1 retained the raw un-stripped namespace segment, making diff_api.py report essentially the entire API (~330 of ~332 entries) as removed-and-readded across v3.10.5->v3.11.0->v3.11.1->v3.11.2 — a bug of the same shape as the earlier USR-arity issue documented in extract_api.py's identity_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 under format_version: 2; only v3.11.0.json and v3.11.1.json actually 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 generated format_version: 2, which is exactly the failure class this versioning exists to catch:

    1. JSON_HAS_RANGES auto-detects via the standard library's __cpp_lib_ranges feature-test macro. That macro 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 used in CI -- so parse()/accept()/from_bjdata()/etc. extracted a different signature (with or without a SentinelType parameter) purely depending on which machine ran the extraction. Now pinned to -DJSON_HAS_RANGES=0 in extract_api.py's parse arguments: the deterministic choice, since forcing 1 was tried first and found to actually fail to parse on a stdlib without full <ranges> support even when the macro claims otherwise.
    2. 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(): 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 identical libclang==18.1.1 wheel and the JSON_HAS_RANGES fix above ruled out as the cause. Now derived from the cursor's own raw source text instead (regex over get_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 under format_version: 3.

Known gaps

  • v3.0.0, v3.0.1: not backfilled. These predate the include/nlohmann/ directory structure entirely — headers lived under src/ at that point (a single src/json.hpp). Since extract_api.py hardcodes include/nlohmann/json.hpp as the entry point, snapshot_release.py --all-tags fails cleanly on these two refs (git archive ... -- include finds nothing) rather than silently producing a wrong/empty result. Not pursued: two tags, immediately superseded by v3.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.py would need a src/json.hpp-aware mode first.
  • Pre-v3.0.0 tags (v1.x, v2.x, v3.0.0-rc*, etc.) were never attempted — out of scope for this backfill; see tools/api_checker/POLICY.md for the stated boundary.