Skip to content

Snapshot Format (.abi.json)

abicheck dump writes a snapshot — a serializable, JSON representation of a library's ABI surface — and abicheck compare reads two snapshots (or a live binary and a saved snapshot) to produce a verdict. Checking a snapshot into your repository as a baseline is the recommended way to detect ABI drift over time (see Baseline Management).

This page documents the snapshot contract: its schema version, its compatibility rules, and its top-level structure.

Snapshots are not reports. A snapshot describes one library's ABI surface. The JSON that compare emits is a separate comparison report with its own version field (report_schema_version). The two are versioned independently — see Two contracts below.


Schema version

Every snapshot carries a top-level schema_version field — a single integer (not MAJOR.MINOR). The current value is 19 (see abicheck/serialization.py's SCHEMA_VERSION for the authoritative, up-to-date value and the full per-version history comment).

{
  "schema_version": 19,
  "library": "libfoo.so.1",
  "version": "1.2.3"
}

The field is placed at the top level so a loader can inspect it without parsing the full snapshot. Its history is additive: each bump added fields without changing the meaning of existing ones — provenance metadata, PE/Mach-O support, build-mode capture, declaration provenance (source_header/origin), embedded build/source evidence, CastXML CV-qualifier reliability, the hybrid AST frontend's per-fact producer map, the resolved AST toolchain identity, (v12) the owner class of a hidden friend (Function.hidden_friend_owner), (v13) the CastXML version-gate outcome (ast_toolchain_supported / ast_toolchain_unsupported_reasons), (v14) extraction-contract fingerprints proving two snapshots were compared under a comparable profile/scope (AbiSnapshot.contract, ADR-050 D1 — verdict-blocking: see the compatibility table below), (v15) structured compile-context provenance for the header-AST parse (ast_resolved_standard, ast_cplusplus_macro, ast_compile_args, ast_sysroot), (v16) DWARF-vs-header-AST layout coherence (dwarf_layout_coherence, dwarf_layout_coherence_mismatches — see "Compile-context provenance" below), (v17) which SYCL/DPC++ AST pass a header-AST snapshot was built from (frontend_context_kind, ADR-050 D5), (v18) whether dump's default toolchain/system-header exclusion was applied (dependency_scope, see dumper_scoping.py), and (v19) whether the direct-clang backend's deprecated/is_scoped facts are reliable (clang_deprecation_facts_reliable, G31 Phase C — those facts became genuinely populated by the clang backend at this version; see dumper_clang.py).

Forward / backward compatibility

abicheck loads a snapshot best-effort and never migrates it in place. The rule is determined entirely by comparing the file's schema_version against the SCHEMA_VERSION the running abicheck supports:

File schema_version Behavior on load
Missing Treated as 1 (the pre-versioning format) and loaded normally.
Older or equal to this build (<= 19) Loaded cleanly. Fields introduced by newer versions are absent and fall back to their defaults (None, empty, or a tri-state None that suppresses the detectors depending on that evidence). No warning.
Newer than this build, and < 14 Loaded best-effort with a UserWarning ("Data may be incomplete or misinterpreted. Upgrade abicheck…"). The load is not aborted — unrecognised keys are ignored and recognised keys are read.
Newer than this build, and >= 14 Hard-rejectedIncompatibleSnapshotSchemaError — instead of warn-and-continue.

Two consequences worth internalising:

  • Reading is version-tolerant in both directions. An older baseline produced by an earlier abicheck loads without error against a newer abicheck; missing fields simply take defaults. This is what makes checked-in baselines durable across tool upgrades.
  • A newer snapshot usually warns rather than fails — but not once a verdict-blocking field exists. Prior to v14 every bump was purely additive, so an older reader can safely ignore a field it doesn't recognise. Starting at v14, AbiSnapshot.contract (ADR-050 D1) makes a bump verdict-blocking: a reader that silently dropped it could compare two possibly-incomparable snapshots and produce an ordinary, wrong verdict. snapshot_from_dict therefore hard-rejects (rather than warns-and-loads) any file schema_version that is both newer than the running build's SCHEMA_VERSION and >= 14 (_MIN_SCHEMA_VERSION_REQUIRING_HARD_REJECTION in abicheck/serialization.py) — this only protects readers built from that guard's introduction onward; see the SCHEMA_VERSION history comment for the full explanation of what it can and cannot retroactively protect. Upgrade abicheck to read a newer snapshot faithfully.

Top-level structure

A snapshot is a single JSON object. The keys below are the ones written by the serializer (abicheck/serialization.py) from the AbiSnapshot model (abicheck/model.py). Optional keys are omitted or null when there is no data (for example, a pure-ELF dump has no dwarf or build_source).

Identity and provenance

Key Type Meaning
schema_version int Snapshot format version (currently 19).
library string Library identity, e.g. libfoo.so.1.
version string Library version string, e.g. 1.2.3.
source_path string | null Original path the snapshot was taken from.
platform string | null elf, pe, macho, or null.
language_profile string | null c, cpp, sycl, or null.
git_commit string | null Git SHA captured at dump time.
git_tag string | null Git tag (e.g. v2.0.0), supplied or auto-detected.
created_at string | null ISO 8601 timestamp set at dump time.
build_id string | null Opaque CI identifier (run ID, build number).
contract object | null ADR-050 D1 extraction-contract fingerprints (schema v14, verdict-blocking — see "Forward / backward compatibility" above): profile_fingerprint/scope_fingerprint plus their named resolved sub-inputs, proving two snapshots were extracted under a comparable profile/scope. null when no producer populated it yet.
dependency_scope string | null (schema v18) "filtered" when the toolchain/system-header exclusion (dumper_scoping.py) was applied, "full" when opted out via --include-dependencies. dump and compare's live-binary dumping (service.run_dump) both filter by default (include_dependencies=False) and tag "filtered"; a Python API caller of service.run_dump/resolve_input gets the opposite default (include_dependencies=True, tagging "full"), preserving every existing caller that doesn't opt in explicitly. scan's own candidate is the one exception: it also filters by default, but derives its actual mode from a --against/--baseline JSON snapshot's own explicit tag (scan_engine._scan_candidate_include_dependencies) — unfiltered only when that baseline is itself explicitly tagged "full", since scan has no --include-dependencies flag of its own to request that directly. null on any pre-v18 snapshot or any snapshot with no header-derived declarations. comparability.check_contracts_comparable raises ScopeMismatchError only when BOTH sides carry an explicit, non-null value and they differ — null is deliberately NOT treated as "full" (an ordinary pre-v18 baseline is usually already-filtered content that simply predates this tag; assuming "full" for it would spuriously flag the routine "compare a cached baseline against a fresh dump" workflow), so a genuinely ambiguous untagged snapshot is left unchecked on this axis rather than guessed at.

Compile-context provenance (schema v15, header-AST parses only)

Populated only when the snapshot came from a header-AST parse (from_headers true); null/empty on a DWARF/symbols-only or binary-only snapshot, and on any pre-v15 snapshot. ast_compile_args and ast_sysroot are redacted via the same RedactionPolicy every L3 build-evidence adapter applies (secret- looking -D values and absolute home-prefixed paths are stripped/normalized before persistence — see abicheck/buildsource/redaction.py).

Key Type Default Meaning
ast_resolved_standard string | null null The C/C++ standard actually used for the header parse: an explicit -std=/--std=//std: value verbatim, or "gnu++20" when the requires/concept heuristic forced it. null means the frontend's own unpinned default was used (never guessed at).
ast_cplusplus_macro string | null null The standard-mandated __cplusplus literal for ast_resolved_standard (e.g. "201703L" for "gnu++17"), looked up from a static ISO-standard table. null when ast_resolved_standard is unset or not a recognized C++ edition.
ast_compile_args array of strings [] The ordered extra compiler arguments passed to the header frontend (--gcc-option tokens, then a shlex-split --gcc-options), redacted.
ast_sysroot string | null null The --sysroot passed to the header frontend, if any, redacted.

ast_toolchain (dict[str, str], populated since schema v9) carries the exact tool identity behind the header-AST parse. It is untyped/free-form — new keys are additive and never require a schema bump — but these keys are stable and machine-checked by tests/test_tool_identity.py/ tests/test_castxml_policy.py:

Key Meaning
selected / compiler_selected The exact frontend/host-compiler executable path selected from PATH (or an explicit --gcc-path).
realpath / compiler_realpath The same path with symlinks resolved.
sha256 / compiler_sha256 SHA-256 of the executable's file contents, so a same-version binary rebuild/repackage still changes provenance.
version / compiler_version The raw, bounded --version transcript for that exact executable revision.
target_triple / compiler_target_triple The <tool> -dumpmachine output for that executable (GCC/G++/Clang/Clang++ only — omitted, not empty, for a tool that doesn't support the flag, e.g. castxml itself or MSVC cl.exe).
castxml_version CastXML's own release version (e.g. "0.7.0"), parsed from version — castxml-producer snapshots only.
castxml_bundled_clang_version The bundled/linked Clang's major.minor (e.g. "18.1"), parsed from version — castxml-producer snapshots only. Kept separate from castxml_version since the two floors (MIN_CASTXML, MIN_CASTXML_CLANG_MAJOR in castxml_policy.py) are independently enforced by the version gate.

A hybrid snapshot (ast_producer == "hybrid") namespaces every key from both runs instead of picking one — castxml_selected, castxml_version, castxml_castxml_version, clang_selected, clang_target_triple, and so on (dumper_hybrid.py's merge is a generic castxml_/clang_-prefixed dict union, so a key that already started with castxml_ on the castxml side is not special-cased).

DWARF-vs-header-AST layout coherence (schema v16)

The clang L2 header backend is layout-blind (no size_bits/alignment_bits/ field offset_bits) — when the binary being dumped also carries DWARF debug info, dumper_layout_backfill.backfill_dwarf_layout() backfills that layout from the same binary's DWARF, but only for a record it can corroborate as the same declaration (matching name, kind, and field/base overlap — see that function's docstring for the exact rules). These two fields make that corroboration outcome visible instead of silent; they never change what gets backfilled, only report on it.

Key Type Default Meaning
dwarf_layout_coherence string | null null One of "matched" (every record eligible for backfill was corroborated, or none needed it), "partial" (some corroborated, some had no DWARF candidate at all — benign, e.g. declared-but-never-instantiated), "mismatch" (at least one record found a uniquely-named DWARF candidate but the two disagreed — backfill already refused to merge that record's layout), or "unavailable" (the clang backend ran but the binary carried no usable DWARF at all). null on any snapshot not built via the clang L2 backend (a castxml snapshot computes layout directly — not a coherence question) and on any pre-v16 snapshot.
dwarf_layout_coherence_mismatches array of strings [] Header record names backfill found a uniquely-named DWARF candidate for but rejected as uncorroborated — populated only when dwarf_layout_coherence == "mismatch".

SYCL/DPC++ frontend context (schema v17, header-AST parses only)

Key Type Default Meaning
frontend_context_kind string | null null Which AST pass ("host" or "device") this header-AST snapshot's clang backend selected via --frontend-context (ADR-050 D5, sycl_context.py). null on any non-SYCL/DPC++ invocation and on any pre-v17 snapshot.

ABI surface

Key Type Meaning
functions array Exported functions (name, mangled name, return type, params, virtuality, access, provenance).
variables array Exported global/static variables.
types array Records (struct/class/union) with fields, bases, vtable, and layout descriptors.
enums array Enumerations with members and underlying type.
typedefs object Typedef name → underlying type.
constants object Preprocessor/compile-time constants (name → value).

Evidence-tier and mode flags

Key Type Meaning
elf_only_mode bool True when dumped without headers (all functions carry ELF-only provenance).
from_headers bool True when the surface was parsed from public headers (drives the header-aware evidence tier). Omitted from the file when it was only inferred on load, so a reload re-runs the same inference.
scope_fallback string | null Public-scope fallback marker.
parsed_with_build_context bool True when parsed with build-context evidence (ADR-029).

Platform and debug metadata (optional)

Key Type Meaning
elf object | null ELF metadata: SONAME, DT_NEEDED, version defs/reqs, symbols, imports, hardening flags.
pe object | null PE/COFF metadata (Windows DLL exports, machine, characteristics).
macho object | null Mach-O metadata (dylib exports, CPU slices, install name).
dwarf object | null DWARF struct/enum layout.
dwarf_advanced object | null Toolchain, calling conventions, value-ABI traits.
sycl object | null SYCL plugin-interface metadata.
dependency_info object | null Resolved dependency graph (nodes, edges, unresolved).
build_mode object | null Normalized compiler/stdlib/standard capture (ADR build-mode work).

Embedded build/source evidence (optional)

Key Type Meaning
build_source_pack object | null Reference to an out-of-band build/source pack (ADR-028). Older snapshots may store this under the legacy key evidence_pack, which the loader still reads.
build_source object | null Inline-embedded build/source facts for single-artifact workflows. Omitted when nothing was embedded.
build_context_defines array of strings The build's active -D macro set, harvested from a compile database (ADR-039). Empty when no compile database was supplied.
conditional_fields object {type: {field: {guard, type, is_bitfield, ...}}} registry of record fields guarded by a single positive #ifdef/#if defined(...), including fields a context-free header parse pruned from types[].fields (ADR-039). Feeds the opt-in --reconcile-build-context diff pass; empty when no compile database was supplied at dump time.

Internal cache fields on the model (_func_by_mangled, _var_by_mangled, _type_by_name) and the runtime-only from_headers_inferred qualifier are never serialized.


Two contracts: snapshot vs report

schema_version and report_schema_version are different fields on different files:

Snapshot (dump) Comparison report (compare --format json)
Version field schema_version report_schema_version
Type integer (currently 19) string MAJOR.MINOR (e.g. 1.0)
Describes one library's ABI surface the diff between two snapshots

A snapshot has no report_schema_version, and a report has no schema_version; the two version numbers evolve independently. For the report contract and its stability policy, see Output Formats.


Stability guidance

  • Check baselines into version control. A saved .abi.json is the intended input to compare; storing one per release lets CI diff each build against the last shipped ABI. See Baseline Management.
  • Older baselines stay readable. Because loading fills missing newer fields with defaults, a baseline written by an earlier abicheck compares correctly against a live binary dumped by a newer one — no regeneration required for a routine tool upgrade.
  • Regenerate when you want new evidence. Fields added in a newer schema_version (e.g. build-mode or embedded source evidence) are only present in freshly-dumped snapshots. Re-dump the baseline to benefit from detectors that rely on that evidence.
  • Pin the abicheck version in CI if a UserWarning about a newer schema_version would be treated as an error in your pipeline.

See also