JSON Diff’te Zaman Damgaları ve ID’ler Nasıl Yoksayılır (jq Yazmadan)
Bir REST endpoint’ine yeni bir alan ekliyorsunuz. Snapshot testleriniz kırmızıya dönüyor — alan değiştiği için değil, createdAt, requestId ve traceId değerleri kaydedilen snapshot ile yeniden çalıştırma arasında değiştiği için. On dakika harcayarak diff’in gürültü olduğunu teyit ediyor, testi susturuyor ve devam ediyorsunuz. Bu durum her takım üyesi için, her derlemede, her hafta tekrar ediyor.
Sorun test framework’ü değil. Sorun, ham JSON diff’inin her alanı eşit değerlendirmesidir. Dönen bir UUID, anlamsal içerik taşıyan bir alanla yapısal olarak aynıdır. “Bu yolu yoksay” demenin bir yolu olmadan, diff yüzeyi sunucunun gömdüğü her zaman damgasını, her otomatik artırılan ID’yi, her istek başına oluşturulan korelasyon belirtecini kapsar — bunların hiçbiri API sözleşmesinin değişip değişmediğini yansıtmaz.
Bu kılavuz, bu gürültüyü kaynakta bastırmanıza olanak tanıyan araçları ve kalıpları ele alır: JSON Pointer yol sözdizimi, diziler için joker karakter kalıpları, anahtar tabanlı eşleştirme için TypeScript yardımcıları, Vitest snapshot entegrasyonu ve yalnızca diff gerçekten önemli bir şey içerdiğinde sıfırdan farklı çıkan küçük bir CI betiği.
”Gürültülü Diff” Sorunu — CI Neden İlgisiz Değişikliklerde Başarısız Olur?
Tipik bir API regresyon iş akışını düşünün. GET /orders/42 isteğine verilen yanıtı kaydediyorsunuz:
{
"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"
}
Bu yanıtı snapshot olarak alıyorsunuz. İki hafta sonra bir meslektaşınız shippingCarrier alanını ekliyor ve test paketini çalıştırıyor. Diff altı değişen satır gösteriyor — dördü sunucu tarafında dönen zaman damgaları ve ID’ler. Tek anlamlı değişiklik (yeni alan) gürültünün içine gömülmüş durumda. Meslektaşınız hiçbir şeyin bozulmadığını doğruluyor, snapshot’ı güncelliyor ve inceleme sürecinde kimse gerçek anlamda işlevsel diff’i okumuyor.
Şimdi bunu yüz endpoint’lik bir mikroservis ağına çarpın. Her derlemede CI, requestId, traceId, version, etag, x-request-start, serverTime içeren snapshot dosyalarını diff’ler. Gözden geçirenler snapshot diff’lerine karşı duyarsız hale gelir ve okumadan onaylar. Regresyon sinyali çöker.
Çözüm, snapshot testini durdurmak değil. Diff çalışmadan önce gürültüyü temizlemektir.
Gürültü Sayılanlar: Zaman Damgaları, UUID’ler, Otomatik ID’ler, Hash’ler
Her değişen alan gürültü değildir. processing’den shipped’e geçen bir status sinyaldir. Soru şudur: tasarım gereği döndükleri için yapısal olarak regresyon bilgisi taşıyamayan alanlar hangileridir?
| Kategori | Örnek alan adları | Neden değişir |
|---|---|---|
| Zaman damgaları | createdAt, updatedAt, deletedAt, lastSeen, expiresAt | Sunucu saati, istek anı |
| İstek korelasyonu | requestId, traceId, spanId, correlationId | İstek başına oluşturulur |
| Otomatik oluşturulan ID’ler | id, uuid, nonce, idempotencyKey | Her ekleme için UUID v4 / ULID / KSUID |
| ETag’ler / önbellek belirteçleri | etag, eTag, cacheKey, version (otomatik artırıldığında) | İçerik hash’i veya sıra numarası |
| Derleme meta verileri | buildHash, deployId, serverVersion, hostname | Dağıtım anında belirlenir |
| Sayfalandırma imleçleri | nextCursor, prevCursor, pageToken | Kodlanmış durum, opak |
Bu tablodaki alanlar yoksayma listesi için adaydır. Gerçek alan durumunu kodlayan alanlar — status, amount, userId, items — asla bu listede yer almamalıdır.
Amaç, kodda yaşayan, PR’larda gözden geçirilen ve güvenlik gereksinimleri değiştiğinde denetlenen bir yoksayma listesidir. Birinin yerel olarak yazıp commit etmeyi unuttuğu tek seferlik bir jq pipeline’ı değil.
JSON Pointer (RFC 6901) Sözdizimi Özeti
RFC 6901’de tanımlanan JSON Pointer, JSON Patch (RFC 6902) ve çoğu yapılandırılmış diff aracı tarafından kullanılan adresleme şemasıdır. Bir dosya sistemi yoluna benzer ve bir JSON belgesindeki belirli bir konumu adresler.
/ → kök belge
/foo → "foo" nesne anahtarı
/foo/bar → "foo" içindeki iç içe "bar" anahtarı
/items/0 → "items" dizisinin ilk elemanı
/items/0/name → ilk elemanın içindeki "name" anahtarı
İki kaçış dizisi özel karakterleri işler:
~0gerçek bir~karakterini temsil eder~1gerçek bir/karakterini temsil eder
Dolayısıyla a/b anahtarı /a~1b olarak, a~b anahtarı ise /a~0b olarak adreslenir.
JSON Pointer, JSONPath’ten (jq, OpenAPI’de kullanılan) temel bir şekilde farklıdır: tam olarak bir konumu adresler — joker karakter yok, özyinelemeli iniş yok, filtre ifadesi yok. RFC 6901 katı ve basittir. Takas şudur: bir alanı tüm dizi elemanlarında yoksaymak için ya spesifikasyonu joker sözdizimi ile genişleten bir araç gerekir ya da pointer yolları programatik olarak yinelemek gerekir.
JSON Patch işlemlerinde path alanlarını gördüğünüzde ([{"op":"replace","path":"/status","value":"shipped"}]), bunlar RFC 6901 pointer’larıdır. fast-json-patch ve rfc6902 gibi araçlar bu gösterimi doğal olarak kullanır.
Joker Karakter Kalıpları: Diziler için /users/*/lastSeen
RFC 6901 joker karakterleri tanımlamaz. Ancak pratikte kullanışlı olan çoğu diff aracı, spesifikasyonu tek segmentli bir joker karakterle genişletir: *, / sınırını geçmeden herhangi bir yol segmentiyle eşleşir.
Extended JSON Pointer Pattern kuralları (Go Tools JSON Diff aracında uygulandığı şekliyle):
*herhangi bir tek yol segmentiyle eşleşir (dizi indeksi veya nesne anahtarı)*,/sınırını geçmez — özyinelemeli glob değildir- Bir anahtar adındaki gerçek asterisk karakteriyle eşleştirmek için
\*olarak kaçış yapın **(çift yıldız özyinelemeli iniş) desteklenmez — açık yollar kullanın
Örnekler:
/users/*/lastSeen → /users/0/lastSeen, /users/1/lastSeen vb. ile eşleşir
/orders/*/updatedAt → orders dizisinin her elemanındaki updatedAt ile eşleşir
/*/requestId → her üst düzey nesne anahtarındaki requestId ile eşleşir
/data/items/*/id → iç içe bir items dizisinin her elemanındaki id ile eşleşir
Tek düzey kısıtlaması önemlidir. /users/*/profile/*/tag geçerlidir ve tam olarak iki düzey joker inişle eşleşir. /users/0/tag (çok sığ) veya /users/0/profile/0/meta/tag (çok derin) ile eşleşmez.
Tipik bir REST API yanıtı için pratik yoksayma listesi:
/requestId
/traceId
/createdAt
/updatedAt
/data/*/createdAt
/data/*/updatedAt
/data/*/id
/pagination/nextCursor
Bu, anlamsal alanlara dokunmadan yaygın durumları kapsar.
Canlı Demo: Stripe Webhook Payload’ında createdAt Yoksayma
Stripe webhook’ları somut bir örnektir. Her olay zarfı created (Unix zaman damgası), id (olay ID’si), request.id ve request.idempotency_key içerir — bunların tümü çağrı başına döner ve regresyon testi için anlamsızdır.
Ham webhook payload’ı:
{
"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"
}
}
}
Bu yapı için yoksayma listesi:
/id
/created
/request/id
/request/idempotency_key
/data/object/id
Bu yoksaymalar uygulandıktan sonra diff yüzeyi yalnızca type, data/object/amount, data/object/currency ve data/object/status alanlarını içerir — ödeme davranışının değişip değişmediğini gerçekten kodlayan alanlar bunlardır.
Hem önceki hem de sonraki payload’ları JSON Diff aracına yapıştırın, yoksanacak yolları girin ve JSON Patch çıktısında yalnızca anlamlı işlemler kalsın. jq pipeline’ı yok. Ön işlem betiği yok.
Bilinmeyen Anahtarlar için Regex Tabanlı Alan Eşleştirme
JSON Pointer yolları, yapıyı önceden bildiğinizde işe yarar. İki senaryoda yetersiz kalır: dinamik nesne anahtarları (UUID anahtarlı map’ler, kullanıcı başına veri blob’ları) ve “adı Id ile biten herhangi bir alan” kalıpları.
Bunlar için regex tabanlı anahtar eşleştirmeye ihtiyacınız var. İşte bir JSON ağacını dolaşan ve anahtar adı bir kalıpla eşleşen tüm pointer yollarını toplayan bir TypeScript yardımcısı:
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;
}
// Usage: collect all paths whose key name ends in "Id" or "At"
const noisePattern = /(?:Id|At|Stamp|Hash|Token|Cursor)$/;
const ignorePaths = findPathsByKeyPattern(responseJson, noisePattern);
// → ["/requestId", "/data/0/createdAt", "/data/0/updatedAt", ...]
Bu, manuel olarak tutulan bir dize kümesi yerine gerçek yanıt yapısından türetilen dinamik bir yoksayma listesi sağlar. Uyarı: userId veya orderId gibi meşru bir alan Id ile bitiyorsa, kalıbı iyileştirmeniz ya da bu yolları sonuçtan çıkarmanız gerekir. Ad kalıpları, açık yol listelerinin yerini almaz — her yolu manuel olarak tutmanın pratik olmadığı büyük API’lerde tamamlayıcı bir yöntemdir.
Yoksanmış Yollarla Temizlenmiş JSON Patch Çıktısı
JSON Patch (RFC 6902) olarak ifade edilen bir JSON diff, işlemler listesidir:
[
{ "op": "replace", "path": "/updatedAt", "value": "2026-05-04T09:00:00Z" },
{ "op": "replace", "path": "/status", "value": "shipped" },
{ "op": "add", "path": "/shippingCarrier", "value": "FedEx" }
]
Yoksanmış yolları filtrelemek, path’i yoksayma listenizdeki herhangi bir girişle eşleşen işlemleri kaldırmak anlamına gelir (joker karakterler hesaba katılarak). Filtrelenmiş çıktı:
[
{ "op": "replace", "path": "/status", "value": "shipped" },
{ "op": "add", "path": "/shippingCarrier", "value": "FedEx" }
]
İki önemli uyarı:
-
Tam geri dönüşüm uyarısı. Filtrelenmiş bir patch, hedef belgeyi yeniden oluşturmak için uygulanamaz. Yalnızca gürültü olmayan işlemleri kaynağa uygularsanız, zaman damgası güncellemelerini içermeyen bir belge elde edersiniz. Belge yeniden oluşturma için tam patch’i saklayın; filtrelenmiş patch’i yalnızca diff ve uyarı amacıyla kullanın.
-
Sıra duyarlılığı. JSON Patch işlemleri sıralıdır. Önceki konumlara başvuran move veya copy işlemlerini içeren bir patch’in ortasındaki işlemleri filtrelemek geçersiz bir patch üretebilir. Filtrelenmiş patch’i uygulamanız gerekiyorsa, önce tam patch’i uygulayın, ardından sonucu beklenen durumunuza göre yeniden diff’leyin.
JSON Diff aracı, hem tam patch’i hem de filtrelenmiş görünümü çıkarır; böylece hangi işlemleri bastıracağınıza karar vermeden önce ham diff’i inceleyebilirsiniz.
Snapshot Test Tarifleri (Jest / Vitest)
Vitest’in snapshot sistemi değerleri string’e serileştirir ve yeniden çalıştırmada bunları diff’ler. Varsayılan serileştirici gürültülü olanlar dahil her alanı yakalar. Çözüm, snapshot almadan önce yoksanmış yolları temizleyen özel bir serileştiricidir.
// 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)),
});
vitest.config.ts dosyasına kaydedin:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
setupFiles: ["./tests/setup/json-diff-serializer.ts"],
},
});
Artık snapshot’larınız şöyle görünür:
it("returns order with shipping carrier", async () => {
const result = await getOrder("ord_42");
expect(result).toMatchSnapshot(); // createdAt, requestId etc. are stripped
});
Snapshot dosyası yalnızca kararlı alanları içerir. Bir zaman damgası değişikliğinin ardından paketi yeniden çalıştırmak sıfır diff üretir. shippingCarrier alanı eklendiğinde, snapshot diff tam olarak o tek değişikliği gösterir. Snapshot’ları güncellemeden önce iki kaydedilmiş yanıt arasında neyin değiştiğini manuel olarak doğrulamak için JSON Diff aracını da kullanabilirsiniz.
CI Entegrasyonu: Derlemeleri Yalnızca Anlamlı Diff’lerde Başarısız Kılma
Nihai hedef: bir referans yanıtı indiren, canlı endpoint’e istek gönderen, ikisini diff’leyen, gürültüyü temizleyen ve yalnızca anlamlı işlemler kalırsa sıfırdan farklı çıkan bir CI adımı.
// 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); });
GitHub Actions adımı:
- name: API regression check
env:
API_URL: ${{ secrets.STAGING_API_URL }}
run: npx tsx scripts/api-regression-check.ts
fast-json-patch’ten gelen compare fonksiyonu RFC 6902 işlemlerini döndürür. Filtre, Vitest serileştiriciyle aynı regex yaklaşımını kullanarak path string’leri üzerinde çalışır. Betik, yalnızca gürültü değiştiyse 0, anlamlı bir alan farklılaştıysa 1 ile çıkar.
Joker karakter eşleştirmesi için (/data/\d+\/updatedAt yerine /data/*/updatedAt kullanmak), regex listesini glob-regex dönüştürücüyle değiştirin ya da JSON Diff aracının kabul ettiği yoksayma listesi biçimini kullanın ve dışa aktarma yoluyla pipeline’ınıza entegre edin.
Ne Zaman Yoksamamalı: Güvenlik Açısından Kritik Alanlar, Denetim İzleri
Yoksayma listesi güçlüdür. Disiplinsiz uygulandığında tehlikeli de olabilir. Bazı alanlar gürültü gibi görünür ama güvenlik açısından kritik bilgiler taşır.
Yoksamamanız gerekenler:
- Denetim günlükleri veya finansal kayıtlardaki
createdAt. Bir işlem zaman damgası yanlışsa bu bir hata — gürültü değil. Denetim tabloları, tam da zaman damgası anomalileri yoluyla manipülasyonu tespit etmek için vardır. - Ödeme niyetleri, faturalar veya düzenleyici kayıtlardaki
idalanları. Bir ödeme bağlamında dönen bir ID, mükerrer ödemeleri veya hatalı yönlendirilen olayları gösterebilir. - İyimser eşzamanlılık sistemlerindeki
versionveyaetag. İki işlem aynı version ile yazarsa biri başarısız olacaktır. Version alanı dekoratif değildir. - Dağıtık sistem hatalarını ayıklarken
traceIdveyaspanId. Bunlar snapshot testlerinde gürültüdür ama üretim olay analizinde sinyaldir. - Güvenlik ekibinizin izlenmesi için işaretlediği herhangi bir alan — SIEM kuralları, uyumluluk gereksinimleri ve veri saklama politikaları bu alanların diff görünür olmasına bağlı olabilir.
Doğru model, yoksayma listesini kod incelemesinden geçen bir izin listesi olarak ele almaktır: depoya commit edilmiş, her yolun neden listede olduğunu açıklayan bir yorum içeren, adlandırılmış bir sabit veya yapılandırma dosyası. Yoksayma listesindeki değişiklikler, üretim yapılandırmasındaki değişikliklerle aynı incelemeyi gerektirmelidir.
// Explicit, auditable, requires PR review to change
export const SNAPSHOT_IGNORE_PATHS = [
"/requestId", // per-request UUID, no semantic content
"/traceId", // OpenTelemetry trace ID, rotates per request
"/createdAt", // server clock, stable in production fixtures
// "/amount" // NEVER ignore — financial field
] as const;
Bir alanın yoksayma listesine girip girmeyeceğinden emin değilseniz, diff içinde tutma yönünde tercih yapın. Yanlış pozitif (diff’teki gürültü) birkaç saniyelik inceleme maliyeti getirir. Yanlış negatif (yol yoksandığı için kaçırılan bir regresyon) kullanıcılarınıza mal olur.
Özet
Ham JSON diff’leri, sunucu yanıtlarının tasarım gereği dönen alanlar içermesi nedeniyle gürültülüdür. JSON Pointer (RFC 6901) sözdizimi size kesin bir adresleme şeması sunar. Joker karakter kalıpları (/data/*/createdAt), özel gezinme kodu yazmadan bunu dizilere genişletir. Regex tabanlı anahtar eşleştirme, yolların önceden bilinmediği dinamik yapıları ele alır. Yoksanmış yolları temizlenmiş JSON Patch (RFC 6902) çıktısı, CI için net bir sinyal üretir. Vitest özel serileştirici, aynı mantığı snapshot testine uygular. Disiplinli, kod incelemesinden geçen bir yoksayma listesi, gürültü bastırmanın gerçek bir regresyonu yanlışlıkla yutmamasını güvence altına alır.
JSON Diff aracında deneyimleyin — iki JSON yanıtını yapıştırın, yoksayma yollarınızı girin ve yalnızca anlamlı işlemleri görün. Diff etmeden önce küçültülmüş yanıtları temizlemek için JSON Biçimlendirici’yi kullanın.