Skip to main content
Version: v2

API Diff (v1 vs v2)

The docs site ships an interactive API Diff modal that compares the v1 and v2 OpenAPI specs side-by-side, grouping changes by tag, path, and request/response object. It lives in the Navbar and is visible to all non-merchant audiences.


Architecture

Navbar ("v1 vs v2" button)
└─ ApiDiffModal.js [src/components/ApiDiff/]
└─ GET /api-diff.json (static asset — pre-generated at build time)
└─ scripts/generate-api-diff.js
├─ parse openapi/apispec.yaml (v1)
└─ parse openapi/v2/apispec.yaml (v2)

The diff is pre-generated into static/api-diff.json and served as a plain static file. There is no live /diff/compare endpoint involved at runtime. Run yarn generate-api-diff to regenerate after spec changes.


Files

FilePurpose
src/components/ApiDiff/ApiDiffModal.jsReact modal — layout, grouping, MOVED detection
src/components/ApiDiff/ApiDiff.cssAll adm-* scoped styles
src/theme/Navbar/index.jsRenders the v1 vs v2 trigger button
scripts/compare.jsDiffs two OpenAPI specs; returns raw JSON
scripts/generate-api-diff.jsSlims the raw diff and writes static/api-diff.json
static/api-diff.jsonPre-generated static asset loaded by the modal
scripts/test-moves.jsUnit tests for collapseDescendants and detectMoves

UI hierarchy

The modal renders a three-level collapsible tree:

TagGroup          (e.g. "Merchant Wallet Manegement")
└─ PathGroup (e.g. "payments" — last static segment before first {param})
└─ EndpointRow (e.g. "POST /v2/payments/{id}/capture")
├─ SummaryBar (counts: +2 -1 ~3)
├─ Status Codes (collapsible per code)
├─ Request Body (ObjectGroupPanel per root segment)
└─ Response Body (ObjectGroupPanel per root segment)

getPathGroup(path) extracts the path group label: it returns the last static segment before the first {param}, or the first segment if there are no params.

// /v2/payments/{id}/capture  →  "payments"
// /v2/customers → "customers"

Endpoints that are fully added or removed show a single-line bulk notice and suppress all field details.


Object grouping — groupIntoObjects()

scripts/compare.jsextractSchemaProps() produces a flat list of fields with dot-path name values (e.g. paymentAllocations[].paymentMethod.id). The modal groups these by the first dot-path segment into object groups (e.g. paymentMethod, paymentAllocations, amount).

Each group carries counts: { added, removed, changed, unchanged } and isComplex (more than 3 fields).


Bulk object notices — ObjectGroupPanel

When every field in a group shares the same status:

  • All added → single ADDED pill, field count
  • All removed → single REMOVED pill, field count
  • Group-level MOVED → purple MOVED pill + → destination hint (see below)

Otherwise the group renders a CollapsibleSection (closed by default) with a field table.


MOVED detection — detectMoves()

detectMoves(objects) returns movedMap: { [fieldOrGroupName]: { from, to } }.

Level 1 — group-level

If an entire object group is fully removed, scan all added fields (including descendants) across every group. If any added field's last segment (after stripping []) equals the removed group's objectName, the group is marked MOVED.

Example: paymentMethod group fully removed (all fields removed). An added field paymentAllocations[].paymentMethod has last segment paymentMethodmovedMap['paymentMethod'] = { from: '(root)', to: 'paymentAllocations' }.

Level 2 — field-level

Uses collapseDescendants() to suppress child rows when an ancestor is already removed/added, then matches collapsed removed fields against collapsed added fields at a different parent:

  • Complex/object fields: match when lastSegment is the same and parents differ.
  • Scalar fields with depth ≥ 2: match when the last-two-segments are the same and parents differ.

Fields already covered by a Level 1 group match are excluded from Level 2.


collapseDescendants()

Strips child rows from a field list when their ancestor is already listed as added or removed. This prevents redundant rows (e.g. paymentMethod.id when paymentMethod itself is removed) and keeps Level 2 matching clean.

paymentMethod          removed  ← kept
paymentMethod.id removed ← suppressed (ancestor is removed)
paymentMethod.card removed ← suppressed
amount unchanged ← kept (no removed/added ancestor)

Running the tests

node scripts/test-moves.js

scripts/test-moves.js contains 9 assertions covering:

  • collapseDescendants — children of removed parent, descendants of added parent, children of changed parent (kept)
  • detectMoves — group-level move, scalar field-level move, false-positive guard (same-name fields in unrelated groups must not match)

Regenerating the static JSON

yarn generate-api-diff

Run this whenever an OpenAPI spec changes. The script is also wired into prebuild so it runs automatically with yarn build.

The generator (scripts/generate-api-diff.js) strips everything the UI doesn't use:

StrippedReason
description on fieldsNot rendered
enum, format, nullable, default, isArray on fieldsNot rendered
Full v1/v2 endpoint copiesOnly operationId (for search) is kept
Flat added/removed/changed arraysReplaced by pre-computed objects groups
Unchanged field rows inside changed groupsNot visible in the UI
Entirely unchanged object groupsNothing to show
fieldDiff for unchanged status codesNo changes to render

Resulting file: ~650 KB (gzips to ~120 KB over the wire).

Status pill colours are defined in the STATUS_STYLES constant at the top of ApiDiffModal.js. The moved entry uses purple (#f3e5f5 / #6a1b9a). Add new entries there and use the adm-delta--{status} CSS class pattern in ApiDiff.css.


CSS namespace

All styles use the adm- prefix (API Diff Modal) to avoid collisions with Docusaurus or other component styles.

ClassDescription
.adm-tag-group / .adm-tag-headerTag-level collapsible group
.adm-path-group / .adm-path-headerPath sub-group
.adm-object-bulk-rowSingle-line notice for fully added/removed/moved objects
.adm-delta--movedPurple MOVED badge
.adm-field-row--movedPurple left-border on a moved field row
.navbar-api-diff-btnNavbar trigger button