Skip to content
Torna al blog
Tutorial

Come Ignorare Timestamp e ID nel Diff JSON (Senza Scrivere jq)

Filtra il rumore nei JSON diff online: ignora timestamp, ID richiesta e UUID con pattern Extended JSON Pointer e isola solo le modifiche reali.

12 min di lettura

Come Ignorare Timestamp e ID nel Diff JSON (Senza Scrivere jq)

Aggiungi un nuovo campo a un endpoint REST. I tuoi snapshot test diventano rossi — non perché il campo sia cambiato, ma perché createdAt, requestId e traceId sono mutati tra lo snapshot registrato e la nuova esecuzione. Passi dieci minuti a confermare che il diff è rumore, metti a tacere il test e vai avanti. Questo si ripete per ogni membro del team, ad ogni build, ogni settimana.

Il problema non è il framework di test. Il problema è che il diff JSON grezzo tratta ogni campo in modo identico. Un UUID rotante è strutturalmente identico a un campo con significato semantico. Senza un modo per dire “ignora questo percorso”, la superficie del diff include ogni timestamp che il server incorpora, ogni ID auto-incrementato, ogni token di correlazione per-richiesta — nessuno dei quali indica se il contratto API sia cambiato.

Questa guida illustra gli strumenti e i pattern che ti permettono di sopprimere quel rumore alla fonte: la sintassi JSON Pointer, i pattern wildcard per gli array, gli helper TypeScript per il matching basato su chiave, l’integrazione con gli snapshot di Vitest e un piccolo script CI che esce con codice non-zero solo quando il diff contiene qualcosa di realmente rilevante.

Il Problema del “Diff Rumoroso” — Perché la CI Fallisce su Modifiche Irrilevanti

Considera un tipico flusso di lavoro di regressione API. Registri la risposta di 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"
}

Scatti uno snapshot di quella risposta. Due settimane dopo, un collega aggiunge un campo shippingCarrier ed esegue la suite. Il diff mostra sei righe modificate — quattro sono timestamp e ID ruotati lato server. La sola modifica significativa (il nuovo campo) è sepolta nel rumore. Il tuo collega conferma che nulla è rotto, aggiorna lo snapshot e la review procede senza che nessuno abbia effettivamente letto il diff funzionale.

Ora moltiplica questo su una rete di microservizi con cento endpoint. Ad ogni build, la CI esegue il diff dei file snapshot che contengono requestId, traceId, version, etag, x-request-start, serverTime. I revisori diventano insensibili ai diff degli snapshot e li approvano senza leggere. Il segnale di regressione collassa.

La soluzione non è smettere di fare snapshot testing. È eliminare il rumore prima che il diff venga eseguito.

Cosa Conta come Rumore: Timestamp, UUID, Auto-ID, Hash

Non ogni campo che muta è rumore. Uno status che cambia da processing a shipped è un segnale. La domanda è quali campi sono strutturalmente incapaci di portare informazioni di regressione perché ruotano per design.

CategoriaNomi di campo di esempioPerché muta
TimestampcreatedAt, updatedAt, deletedAt, lastSeen, expiresAtClock del server, momento della richiesta
Correlazione richiestarequestId, traceId, spanId, correlationIdGenerato per ogni richiesta
ID auto-generatiid, uuid, nonce, idempotencyKeyUUID v4 / ULID / KSUID ad ogni inserimento
ETags / token di cacheetag, eTag, cacheKey, version (se auto-incrementato)Hash del contenuto o sequenza
Metadati di buildbuildHash, deployId, serverVersion, hostnameImpostato al momento del deploy
Cursori di paginazionenextCursor, prevCursor, pageTokenStato codificato, opaco

I campi in questa tabella sono candidati per la lista di esclusione. I campi che codificano lo stato reale del dominio — status, amount, userId, items — non dovrebbero mai esservi.

L’obiettivo è una lista di esclusione che vive nel codice, viene esaminata nelle PR e viene verificata quando i requisiti di sicurezza cambiano. Non una pipeline jq usa-e-getta che qualcuno ha scritto localmente e si è dimenticato di committare.

Introduzione alla Sintassi JSON Pointer (RFC 6901)

JSON Pointer, definito in RFC 6901, è lo schema di indirizzamento usato da JSON Patch (RFC 6902) e dalla maggior parte degli strumenti di diff strutturato. Assomiglia a un percorso di filesystem e indirizza una posizione specifica in un documento JSON.

/               → documento radice
/foo            → chiave oggetto "foo"
/foo/bar        → chiave annidata "bar" dentro "foo"
/items/0        → primo elemento dell'array "items"
/items/0/name   → chiave "name" nel primo elemento

Due sequenze di escape gestiscono i caratteri speciali:

  • ~0 rappresenta un ~ letterale
  • ~1 rappresenta uno / letterale

Quindi la chiave a/b viene indirizzata come /a~1b, e la chiave a~b come /a~0b.

JSON Pointer differisce da JSONPath (usato in jq, OpenAPI) in un aspetto fondamentale: indirizza esattamente una posizione — nessun wildcard, nessuna discesa ricorsiva, nessuna espressione di filtro. RFC 6901 è rigoroso e semplice. Il compromesso è che ignorare un campo su tutti gli elementi di un array richiede o uno strumento che estenda la specifica con la sintassi wildcard, o l’iterazione programmatica dei percorsi.

Quando vedi campi path nelle operazioni JSON Patch ([{"op":"replace","path":"/status","value":"shipped"}]), questi sono puntatori RFC 6901. Strumenti come fast-json-patch e rfc6902 usano questa notazione in modo nativo.

Pattern Wildcard: /users/*/lastSeen per gli Array

RFC 6901 non definisce wildcard. Ma la maggior parte degli strumenti di diff utili nella pratica estende la specifica con un wildcard a singolo segmento: * corrisponde a qualsiasi segmento di percorso senza attraversare /.

Le regole per Extended JSON Pointer Pattern (come implementato nello strumento JSON Diff di Go Tools):

  • * corrisponde a qualsiasi singolo segmento di percorso (indice di array o chiave oggetto)
  • * non attraversa / — non è un glob ricorsivo
  • Per corrispondere a un asterisco letterale nel nome di una chiave, si usa \* come escape
  • ** (discesa ricorsiva con doppio asterisco) non è supportato — usa percorsi espliciti

Esempi:

/users/*/lastSeen        → corrisponde a /users/0/lastSeen, /users/1/lastSeen, ecc.
/orders/*/updatedAt      → corrisponde a updatedAt in ogni elemento dell'array orders
/*/requestId             → corrisponde a requestId in ogni chiave oggetto di primo livello
/data/items/*/id         → corrisponde a id in ogni elemento di un array items annidato

Il vincolo a singolo livello è importante. /users/*/profile/*/tag è valido e corrisponde esattamente a due livelli di discesa wildcard. Non corrisponde a /users/0/tag (troppo superficiale) né a /users/0/profile/0/meta/tag (troppo profondo).

Lista di esclusione pratica per una tipica risposta REST API:

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

Questo copre i casi comuni senza toccare nessun campo semantico.

Demo Live: Ignorare createdAt in un Payload Webhook Stripe

I webhook Stripe sono un esempio concreto. Ogni envelope di evento contiene created (timestamp Unix), id (ID evento), request.id e request.idempotency_key — tutti ruotano ad ogni invocazione e sono inutili per i test di regressione.

Payload webhook grezzo:

{
  "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"
    }
  }
}

Lista di esclusione per questa struttura:

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

Dopo aver applicato queste esclusioni, la superficie del diff contiene solo type, data/object/amount, data/object/currency e data/object/status — i campi che codificano effettivamente se il comportamento del pagamento è cambiato.

Incolla entrambi i payload (prima e dopo) nello strumento JSON Diff, inserisci i percorsi da ignorare e nell’output JSON Patch rimarranno solo le operazioni significative. Nessuna pipeline jq. Nessuno script di pre-elaborazione.

Matching per Regex su Chiavi Sconosciute

I percorsi JSON Pointer funzionano quando conosci la struttura in anticipo. Si rompono in due scenari: chiavi oggetto dinamiche (mappe con chiavi UUID, blob di dati per-utente) e pattern del tipo “qualsiasi campo il cui nome termina in Id”.

Per questi casi, hai bisogno del matching per regex sulle chiavi. Ecco un helper TypeScript che percorre un albero JSON e raccoglie tutti i percorsi i cui nomi di chiave corrispondono a un pattern:

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;
}

// Uso: raccoglie tutti i percorsi il cui nome di chiave termina in "Id" o "At"
const noisePattern = /(?:Id|At|Stamp|Hash|Token|Cursor)$/;
const ignorePaths = findPathsByKeyPattern(responseJson, noisePattern);
// → ["/requestId", "/data/0/createdAt", "/data/0/updatedAt", ...]

Questo ti fornisce una lista di esclusione dinamica derivata dalla forma effettiva della risposta, anziché un insieme di stringhe mantenuto manualmente. L’avvertenza: se un campo legittimo termina in Id (come userId o orderId), devi o raffinare il pattern o sottrarre quei percorsi dal risultato. I pattern di nome non sostituiscono le liste di percorsi espliciti — li complementano per API di grandi dimensioni dove mantenere ogni percorso manualmente è impraticabile.

Output JSON Patch con i Percorsi Ignorati Rimossi

Un diff JSON espresso come JSON Patch (RFC 6902) è una lista di operazioni:

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

Filtrare i percorsi ignorati significa rimuovere le operazioni il cui path corrisponde a qualsiasi voce nella lista di esclusione (tenendo conto dei wildcard). L’output filtrato:

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

Due avvertenze importanti:

  1. Avviso di non-round-trip. Una patch filtrata non può essere applicata per ricostruire il documento di destinazione. Se applichi solo le operazioni non-rumore alla sorgente, ottieni un documento a cui mancano gli aggiornamenti dei timestamp. Conserva la patch completa per la ricostruzione del documento; usa la patch filtrata solo per il diff e gli avvisi.

  2. Sensibilità all’ordine. Le operazioni JSON Patch sono ordinate. Filtrare operazioni dalla metà di una patch che ha operazioni move o copy che fanno riferimento a posizioni precedenti può produrre una patch non valida. Se devi applicare la patch filtrata, applica prima la patch completa, poi riesegui il diff del risultato rispetto allo stato atteso.

Lo strumento JSON Diff restituisce sia la patch completa sia la vista filtrata, così puoi ispezionare il diff grezzo prima di decidere quali operazioni sopprimere.

Ricette per lo Snapshot Testing (Jest / Vitest)

Il sistema di snapshot di Vitest serializza i valori in stringhe e ne esegue il diff ad ogni riesecuzione. Il serializzatore predefinito cattura ogni campo, compresi quelli rumorosi. La soluzione è un serializzatore personalizzato che rimuove i percorsi ignorati prima dello 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)),
});

Registralo in vitest.config.ts:

import { defineConfig } from "vitest/config";

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

Ora i tuoi snapshot hanno questo aspetto:

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

Il file snapshot contiene solo campi stabili. Rieseguire la suite dopo una modifica ai timestamp produce diff zero. Quando il campo shippingCarrier viene aggiunto, il diff dello snapshot mostra esattamente quella sola modifica. Puoi anche usare lo strumento JSON Diff per verificare manualmente cosa è cambiato tra due risposte registrate prima di aggiornare gli snapshot.

Integrazione CI: Build che Falliscono Solo su Diff Significativi

L’obiettivo finale: uno step CI che scarica una risposta di riferimento, interroga l’endpoint live, esegue il diff tra i due, rimuove il rumore ed esce con codice non-zero solo se rimangono operazioni significative.

// 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); });

Step GitHub Actions:

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

La funzione compare di fast-json-patch restituisce operazioni RFC 6902. Il filtro opera sulle stringhe path usando lo stesso approccio regex del serializzatore Vitest. Lo script esce con 0 se è cambiato solo il rumore, con 1 se qualsiasi campo significativo differisce.

Per il matching con wildcard (che copre /data/*/updatedAt invece di /data/\d+\/updatedAt), sostituisci la lista di regex con un convertitore glob-to-regex oppure usa il formato della lista di esclusione accettato dallo strumento JSON Diff e integralo nella tua pipeline tramite l’esportazione.

Quando NON Ignorare: Campi Critici per la Sicurezza, Audit Trail

La lista di esclusione è potente. È anche pericolosa se applicata senza disciplina. Alcuni campi sembrano rumore ma portano informazioni critiche per la sicurezza.

Non ignorare:

  • createdAt in log di audit o registrazioni finanziarie. Se un timestamp di transazione è errato, è un bug — non rumore. Le tabelle di audit esistono precisamente per rilevare le manomissioni tramite anomalie nei timestamp.
  • Campi id in payment intent, fatture o registri normativi. Un ID rotante in un contesto di pagamento potrebbe indicare addebiti duplicati o eventi instradati erroneamente.
  • version o etag in sistemi di concorrenza ottimistica. Se due processi scrivono entrambi con la stessa versione, uno fallirà. Il campo version non è decorativo.
  • traceId o spanId quando si analizzano i guasti di sistemi distribuiti. Sono rumore nei test di snapshot ma segnale nell’analisi degli incidenti in produzione.
  • Qualsiasi campo segnalato dal team di sicurezza per il monitoraggio — le regole SIEM, i requisiti di conformità e le policy di conservazione dei dati possono dipendere dal fatto che questi campi siano visibili nel diff.

Il modello corretto è trattare la lista di esclusione come una allowlist revisionata nel codice: una costante denominata o un file di configurazione, committato nel repository, con un commento che spiega perché ogni percorso è nella lista. Le modifiche alla lista di esclusione dovrebbero richiedere la stessa revisione delle modifiche alla configurazione di produzione.

// Esplicita, verificabile, richiede una PR review per modificarla
export const SNAPSHOT_IGNORE_PATHS = [
  "/requestId",   // UUID per-richiesta, nessun contenuto semantico
  "/traceId",     // ID trace OpenTelemetry, ruota per ogni richiesta
  "/createdAt",   // clock del server, stabile nelle fixture di produzione
  // "/amount"    // NON ignorare mai — campo finanziario
] as const;

Se non sei sicuro se un campo debba essere nella lista di esclusione, opta per mantenerlo nel diff. Un falso positivo (rumore nel diff) costa pochi secondi di revisione. Un falso negativo (una regressione mancata perché il percorso era ignorato) costa agli utenti.

I diff JSON grezzi sono rumorosi perché le risposte del server incorporano campi che ruotano per design. La sintassi JSON Pointer (RFC 6901) fornisce uno schema di indirizzamento preciso. I pattern wildcard (/data/*/createdAt) estendono questo agli array senza scrivere codice di attraversamento personalizzato. Il matching per regex sulle chiavi gestisce le strutture dinamiche dove i percorsi non sono noti in anticipo. L’output JSON Patch (RFC 6902) con i percorsi ignorati rimossi produce un segnale pulito per la CI. Un serializzatore personalizzato Vitest applica la stessa logica allo snapshot testing. E una lista di esclusione disciplinata e revisionata nel codice garantisce che la soppressione del rumore non inghiottisca accidentalmente una regressione reale.

Provalo in modo pratico nello strumento JSON Diff — incolla due risposte JSON, inserisci i percorsi da ignorare e osserva solo le operazioni significative. Usa il Formattatore JSON per ripulire le risposte minificate prima di eseguire il diff.

Articoli correlati

Vedi tutti gli articoli