Skip to content
Kembali ke Blog
Tutorial

Cara Mengabaikan Timestamp dan ID dalam JSON Diff (Tanpa Menulis jq)

Diff regresi API 80% berisi noise — timestamp, request ID, UUID yang berubah per-request. Gunakan pola Extended JSON Pointer untuk menampilkan hanya perubahan yang bermakna.

12 menit baca

Cara Mengabaikan Timestamp dan ID dalam JSON Diff (Tanpa Menulis jq)

Anda menambahkan field baru ke endpoint REST. Snapshot test langsung merah — bukan karena field tersebut berubah, melainkan karena createdAt, requestId, dan traceId berubah antara snapshot yang direkam dan run berikutnya. Anda menghabiskan sepuluh menit memastikan diff hanyalah noise, mematikan test tersebut, lalu melanjutkan. Ini berulang untuk setiap anggota tim, di setiap build, setiap minggu.

Masalahnya bukan pada test framework. Masalahnya adalah raw JSON diff memperlakukan setiap field secara setara. UUID yang terus berganti secara struktural identik dengan field yang membawa makna semantik. Tanpa cara untuk mengatakan “abaikan path ini,” permukaan diff mencakup setiap timestamp yang disematkan server, setiap auto-incremented ID, setiap correlation token per-request — tidak satu pun mencerminkan apakah kontrak API berubah.

Panduan ini membahas tools dan pola yang memungkinkan Anda menekan noise tersebut dari sumbernya: sintaks path JSON Pointer, pola wildcard untuk array, TypeScript helper untuk key-based matching, integrasi snapshot Vitest, dan skrip CI kecil yang keluar non-zero hanya ketika diff mengandung sesuatu yang benar-benar penting.

Masalah “Diff Berisik” — Mengapa CI Gagal pada Perubahan yang Tidak Relevan

Pertimbangkan alur kerja regresi API yang umum. Anda merekam respons dari GET /orders/42:

{
  "id": "ord_9f8e7d6c",
  "status": "shipped",
  "createdAt": "2026-04-01T10:00:00Z",
  "updatedAt": "2026-04-01T12:34:56Z",
  "requestId": "req_abc123",
  "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}

Anda melakukan snapshot pada respons tersebut. Dua minggu kemudian, seorang rekan menambahkan field shippingCarrier dan menjalankan suite. Diff menampilkan enam baris yang berubah — empat di antaranya adalah timestamp dan ID yang berganti di sisi server. Satu-satunya perubahan bermakna (field baru) terkubur dalam noise. Rekan Anda memastikan tidak ada yang rusak, memperbarui snapshot, dan review pun berlanjut tanpa siapa pun benar-benar membaca functional diff.

Sekarang kalikan ini di seluruh microservices mesh dengan ratusan endpoint. Setiap build, CI melakukan diff pada file snapshot yang mengandung requestId, traceId, version, etag, x-request-start, serverTime. Para reviewer menjadi kebal terhadap snapshot diff dan menyetujuinya tanpa membaca. Sinyal regresi pun runtuh.

Solusinya bukan berhenti snapshot testing. Solusinya adalah membuang noise sebelum diff dijalankan.

Apa yang Termasuk Noise: Timestamp, UUID, Auto-ID, Hash

Tidak setiap field yang berubah adalah noise. status yang berubah dari processing ke shipped adalah sinyal. Pertanyaannya adalah field mana yang secara struktural tidak mampu membawa informasi regresi karena memang dirancang untuk berganti.

KategoriContoh nama fieldMengapa berubah
TimestampcreatedAt, updatedAt, deletedAt, lastSeen, expiresAtClock server, waktu request
Request correlationrequestId, traceId, spanId, correlationIdDibuat per-request
Auto-generated IDid, uuid, nonce, idempotencyKeyUUID v4 / ULID / KSUID setiap insert
ETags / cache tokenetag, eTag, cacheKey, version (saat auto-increment)Content hash atau sequence
Build metadatabuildHash, deployId, serverVersion, hostnameDiset saat deploy
Pagination cursornextCursor, prevCursor, pageTokenState ter-encode, bersifat opaque

Field dalam tabel ini adalah kandidat untuk daftar ignore. Field yang menyimpan state domain sesungguhnya — status, amount, userId, items — tidak boleh ada di dalamnya.

Tujuannya adalah daftar ignore yang hidup dalam kode, di-review dalam PR, dan diaudit ketika persyaratan keamanan berubah. Bukan pipeline jq sekali pakai yang ditulis seseorang secara lokal dan lupa di-commit.

Panduan Sintaks JSON Pointer (RFC 6901)

JSON Pointer, yang didefinisikan dalam RFC 6901, adalah skema pengalamatan yang digunakan oleh JSON Patch (RFC 6902) dan sebagian besar tools diff terstruktur. Tampilannya seperti path filesystem dan mengacu pada lokasi tertentu dalam dokumen JSON.

/               → dokumen root
/foo            → object key "foo"
/foo/bar        → key "bar" bersarang di dalam "foo"
/items/0        → elemen pertama array "items"
/items/0/name   → key "name" di dalam elemen pertama

Dua escape sequence menangani karakter khusus:

  • ~0 merepresentasikan literal ~
  • ~1 merepresentasikan literal /

Jadi key a/b dialamatkan sebagai /a~1b, dan key a~b dialamatkan sebagai /a~0b.

JSON Pointer berbeda dari JSONPath (yang digunakan dalam jq, OpenAPI) dalam satu hal penting: ia mengacu pada tepat satu lokasi — tidak ada wildcard, tidak ada recursive descent, tidak ada filter expression. RFC 6901 bersifat ketat dan sederhana. Trade-off-nya adalah mengabaikan field di semua elemen array memerlukan tools yang memperluas spesifikasi dengan sintaks wildcard, atau melakukan iterasi path pointer secara programatis.

Saat Anda melihat field path dalam operasi JSON Patch ([{"op":"replace","path":"/status","value":"shipped"}]), itulah RFC 6901 pointer. Tools seperti fast-json-patch dan rfc6902 menggunakan notasi ini secara native.

Pola Wildcard: /users/*/lastSeen untuk Array

RFC 6901 tidak mendefinisikan wildcard. Namun sebagian besar diff tools yang berguna dalam praktik memperluas spesifikasi dengan wildcard satu segmen: * cocok dengan satu path segment apa pun tanpa melewati /.

Aturan untuk Extended JSON Pointer Pattern (sebagaimana diimplementasikan dalam tools JSON Diff Go Tools):

  • * cocok dengan satu path segment tunggal apa pun (index array atau object key)
  • * tidak melewati / — ini bukan recursive glob
  • Untuk mencocokkan tanda bintang literal dalam nama key, escape dengan \*
  • ** (recursive descent double-star) tidak didukung — gunakan path eksplisit

Contoh:

/users/*/lastSeen        → cocok dengan /users/0/lastSeen, /users/1/lastSeen, dst.
/orders/*/updatedAt      → cocok dengan updatedAt di setiap elemen array orders
/*/requestId             → cocok dengan requestId di setiap object key level atas
/data/items/*/id         → cocok dengan id di setiap elemen array items bersarang

Batasan satu level ini penting. /users/*/profile/*/tag valid dan cocok tepat dua level wildcard descent. Ini tidak cocok dengan /users/0/tag (terlalu dangkal) atau /users/0/profile/0/meta/tag (terlalu dalam).

Daftar ignore praktis untuk respons REST API tipikal:

/requestId
/traceId
/createdAt
/updatedAt
/data/*/createdAt
/data/*/updatedAt
/data/*/id
/pagination/nextCursor

Ini mencakup kasus umum tanpa menyentuh field semantik apa pun.

Demo Langsung: Mengabaikan createdAt dalam Payload Stripe Webhook

Stripe webhook adalah contoh konkret. Setiap event envelope mengandung created (Unix timestamp), id (event ID), request.id, dan request.idempotency_key — semuanya berganti per-invocation dan tidak berguna untuk regression testing.

Payload webhook mentah:

{
  "id": "evt_1OqLmn2eZvKYlo2C9Pt1a4X7",
  "object": "event",
  "created": 1714819200,
  "type": "payment_intent.succeeded",
  "request": {
    "id": "req_Bc7dEfGhIj",
    "idempotency_key": null
  },
  "data": {
    "object": {
      "id": "pi_3OqLmn2eZvKYlo2C0Pt1a4X8",
      "amount": 2000,
      "currency": "usd",
      "status": "succeeded"
    }
  }
}

Daftar ignore untuk bentuk ini:

/id
/created
/request/id
/request/idempotency_key
/data/object/id

Setelah menerapkan ignore ini, permukaan diff hanya mengandung type, data/object/amount, data/object/currency, dan data/object/status — field yang benar-benar menyimpan apakah perilaku pembayaran berubah.

Tempel payload before dan after ke tools JSON Diff, masukkan path ignore, dan hanya operasi bermakna yang tersisa dalam output JSON Patch. Tidak perlu pipeline jq. Tidak perlu skrip pre-processing.

Regex-Based Field Matching untuk Key yang Tidak Diketahui

Path JSON Pointer bekerja saat Anda mengetahui struktur data di awal. Mereka gagal dalam dua skenario: object key dinamis (map dengan UUID key, blob data per-user) dan pola “field apa pun yang namanya berakhiran Id”.

Untuk ini, Anda membutuhkan regex-based key matching. Berikut TypeScript helper yang menelusuri tree JSON dan mengumpulkan semua path pointer yang nama key-nya cocok dengan pola:

type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };

function findPathsByKeyPattern(
  value: JsonValue,
  pattern: RegExp,
  currentPath = ""
): string[] {
  const paths: string[] = [];

  if (Array.isArray(value)) {
    value.forEach((item, i) => {
      paths.push(...findPathsByKeyPattern(item, pattern, `${currentPath}/${i}`));
    });
  } else if (value !== null && typeof value === "object") {
    for (const [key, child] of Object.entries(value)) {
      const encodedKey = key.replace(/~/g, "~0").replace(/\//g, "~1");
      const childPath = `${currentPath}/${encodedKey}`;
      if (pattern.test(key)) {
        paths.push(childPath);
      }
      paths.push(...findPathsByKeyPattern(child, pattern, childPath));
    }
  }

  return paths;
}

// Penggunaan: kumpulkan semua path yang nama key-nya berakhiran "Id" atau "At"
const noisePattern = /(?:Id|At|Stamp|Hash|Token|Cursor)$/;
const ignorePaths = findPathsByKeyPattern(responseJson, noisePattern);
// → ["/requestId", "/data/0/createdAt", "/data/0/updatedAt", ...]

Ini memberi Anda daftar ignore dinamis yang berasal dari bentuk respons aktual, bukan sekumpulan string yang dikelola secara manual. Peringatannya: jika field yang sah berakhiran Id (seperti userId atau orderId), Anda perlu mempersempit pola atau mengurangi path tersebut dari hasilnya. Pola nama bukan pengganti daftar path eksplisit — keduanya saling melengkapi untuk API besar di mana mempertahankan setiap path secara manual tidak praktis.

Output JSON Patch dengan Path yang Diabaikan Dibuang

Diff JSON yang diekspresikan sebagai JSON Patch (RFC 6902) adalah daftar operasi:

[
  { "op": "replace", "path": "/updatedAt", "value": "2026-05-04T09:00:00Z" },
  { "op": "replace", "path": "/status", "value": "shipped" },
  { "op": "add", "path": "/shippingCarrier", "value": "FedEx" }
]

Memfilter path yang diabaikan berarti menghapus operasi yang path-nya cocok dengan entri mana pun dalam daftar ignore Anda (memperhitungkan wildcard). Output yang sudah difilter:

[
  { "op": "replace", "path": "/status", "value": "shipped" },
  { "op": "add", "path": "/shippingCarrier", "value": "FedEx" }
]

Dua catatan penting:

  1. Peringatan non-round-trip. Patch yang sudah difilter tidak dapat diterapkan untuk merekonstruksi dokumen target. Jika Anda hanya menerapkan operasi non-noise ke sumber, Anda mendapat dokumen yang tidak memiliki pembaruan timestamp. Simpan patch lengkap untuk rekonstruksi dokumen; gunakan patch yang difilter hanya untuk diffing dan alerting.

  2. Sensitivitas urutan. Operasi JSON Patch berurutan. Memfilter operasi dari tengah patch yang memiliki operasi move atau copy yang mereferensikan posisi sebelumnya dapat menghasilkan patch yang tidak valid. Jika Anda perlu menerapkan patch yang difilter, terapkan patch lengkap terlebih dahulu, lalu lakukan diff ulang hasilnya terhadap state yang diharapkan.

Tools JSON Diff menghasilkan output patch lengkap dan tampilan yang sudah difilter, sehingga Anda dapat memeriksa raw diff sebelum memutuskan operasi mana yang akan ditekan.

Resep Snapshot Testing (Jest / Vitest)

Sistem snapshot Vitest melakukan serialisasi nilai menjadi string dan melakukan diff saat dijalankan ulang. Serializer default menangkap setiap field, termasuk yang berisik. Solusinya adalah custom serializer yang membuang path yang diabaikan sebelum melakukan snapshot.

// tests/setup/json-diff-serializer.ts
import { expect } from "vitest";

const IGNORE_PATHS = new Set([
  "/requestId",
  "/traceId",
  "/createdAt",
  "/updatedAt",
  "/data/id",
]);

function stripPaths(obj: unknown, path = ""): unknown {
  if (Array.isArray(obj)) {
    return obj.map((item, i) => stripPaths(item, `${path}/${i}`));
  }
  if (obj !== null && typeof obj === "object") {
    const result: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
      const childPath = `${path}/${key.replace(/~/g, "~0").replace(/\//g, "~1")}`;
      if (!IGNORE_PATHS.has(childPath)) {
        result[key] = stripPaths(value, childPath);
      }
    }
    return result;
  }
  return obj;
}

expect.addSnapshotSerializer({
  test: (val) => val !== null && typeof val === "object" && !Array.isArray(val),
  print: (val, serialize) => serialize(stripPaths(val)),
});

Daftarkan dalam vitest.config.ts:

import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    setupFiles: ["./tests/setup/json-diff-serializer.ts"],
  },
});

Sekarang snapshot Anda terlihat seperti:

it("returns order with shipping carrier", async () => {
  const result = await getOrder("ord_42");
  expect(result).toMatchSnapshot(); // createdAt, requestId dll. sudah dibuang
});

File snapshot hanya mengandung field yang stabil. Menjalankan ulang suite setelah perubahan timestamp menghasilkan diff nol. Ketika field shippingCarrier ditambahkan, snapshot diff menampilkan tepat satu perubahan itu saja. Anda juga dapat menggunakan tools JSON Diff untuk memverifikasi secara manual apa yang berubah antara dua respons yang direkam sebelum memperbarui snapshot.

Integrasi CI: Hanya Gagalkan Build pada Diff yang Bermakna

Tujuan akhirnya: langkah CI yang mengunduh respons referensi, menyentuh endpoint live, melakukan diff keduanya, membuang noise, dan keluar non-zero hanya jika operasi yang bermakna tersisa.

// scripts/api-regression-check.ts
import { applyPatch, compare } from "fast-json-patch";
import { readFileSync } from "fs";

const IGNORE_PATTERNS = [
  /^\/requestId$/,
  /^\/traceId$/,
  /^\/createdAt$/,
  /^\/updatedAt$/,
  /^\/data\/\d+\/createdAt$/,
  /^\/data\/\d+\/updatedAt$/,
  /^\/data\/\d+\/id$/,
];

function isMeaningfulOp(op: { path: string }): boolean {
  return !IGNORE_PATTERNS.some((re) => re.test(op.path));
}

async function main() {
  const reference = JSON.parse(readFileSync("reference.json", "utf-8"));
  const response = await fetch(process.env.API_URL + "/orders").then((r) => r.json());

  const ops = compare(reference, response);
  const meaningfulOps = ops.filter(isMeaningfulOp);

  if (meaningfulOps.length > 0) {
    console.error("Meaningful diff detected:");
    console.error(JSON.stringify(meaningfulOps, null, 2));
    process.exit(1);
  }

  console.log(`Clean diff (${ops.length} noise ops suppressed).`);
}

main().catch((e) => { console.error(e); process.exit(1); });

Langkah GitHub Actions:

- name: API regression check
  env:
    API_URL: ${{ secrets.STAGING_API_URL }}
  run: npx tsx scripts/api-regression-check.ts

Fungsi compare dari fast-json-patch mengembalikan operasi RFC 6902. Filter berjalan pada string path menggunakan pendekatan regex yang sama seperti serializer Vitest. Skrip keluar 0 jika hanya noise yang berubah, 1 jika ada field bermakna yang berbeda.

Untuk wildcard matching (mencakup /data/*/updatedAt alih-alih /data/\d+\/updatedAt), ganti daftar regex dengan konverter glob-to-regex atau gunakan format daftar ignore yang diterima oleh tools JSON Diff dan integrasikan ke dalam pipeline Anda melalui ekspornya.

Kapan TIDAK Mengabaikan: Field Sensitif Keamanan, Audit Trail

Daftar ignore sangat powerful. Namun juga berbahaya jika diterapkan tanpa disiplin. Beberapa field terlihat seperti noise tetapi membawa informasi yang kritis dari sisi keamanan.

Jangan abaikan:

  • createdAt dalam log audit atau catatan keuangan. Jika timestamp transaksi salah, itu adalah bug — bukan noise. Tabel audit ada justru untuk mendeteksi gangguan melalui anomali timestamp.
  • Field id dalam payment intent, invoice, atau catatan regulasi. ID yang berganti dalam konteks pembayaran mungkin mengindikasikan charge duplikat atau event yang salah arah.
  • version atau etag dalam sistem optimistic concurrency. Jika dua proses sama-sama menulis dengan versi yang sama, salah satunya akan gagal. Field version bukan hiasan.
  • traceId atau spanId saat melakukan debug kegagalan sistem terdistribusi. Ini adalah noise dalam snapshot test tetapi sinyal dalam analisis insiden produksi.
  • Field apa pun yang telah ditandai oleh tim keamanan Anda untuk pemantauan — aturan SIEM, persyaratan kepatuhan, dan kebijakan retensi data mungkin bergantung pada field ini yang terlihat dalam diff.

Model yang tepat adalah memperlakukan daftar ignore sebagai allowlist yang di-review oleh kode: konstanta bernama atau file konfigurasi, di-check in ke repository, dengan komentar yang menjelaskan mengapa setiap path ada di dalamnya. Perubahan pada daftar ignore harus memerlukan review yang sama seperti perubahan pada konfigurasi produksi.

// Eksplisit, dapat diaudit, memerlukan PR review untuk diubah
export const SNAPSHOT_IGNORE_PATHS = [
  "/requestId",   // per-request UUID, tidak ada konten semantik
  "/traceId",     // OpenTelemetry trace ID, berganti per request
  "/createdAt",   // clock server, stabil dalam fixture produksi
  // "/amount"    // JANGAN PERNAH diabaikan — field keuangan
] as const;

Jika Anda tidak yakin apakah sebuah field harus ada di daftar ignore, pilih untuk tetap menyertakannya dalam diff. False positive (noise dalam diff) menghabiskan beberapa detik review. False negative (regresi terlewat karena path diabaikan) merugikan pengguna Anda.

Ringkasan

Raw JSON diff berisik karena respons server menyematkan field yang memang dirancang untuk berganti. Sintaks JSON Pointer (RFC 6901) memberi Anda skema pengalamatan yang presisi. Pola wildcard (/data/*/createdAt) memperluas itu ke array tanpa menulis kode traversal kustom. Regex-based key matching menangani struktur dinamis di mana path tidak diketahui sebelumnya. Output JSON Patch (RFC 6902) dengan path yang diabaikan dibuang menghasilkan sinyal bersih untuk CI. Custom serializer Vitest menerapkan logika yang sama pada snapshot testing. Dan daftar ignore yang disiplin dan di-review oleh kode memastikan penekan noise tidak secara tidak sengaja menelan regresi nyata.

Coba langsung di tools JSON Diff — tempel dua respons JSON, masukkan path ignore Anda, dan lihat hanya operasi bermakna. Gunakan JSON Formatter untuk membersihkan respons yang diminifikasi sebelum melakukan diff.

Artikel Terkait

Lihat semua artikel