Staging and production can return different JSON for the same request even when the code is identical: timestamps, request IDs, array order and number formatting all change. A text diff then reports noise and hides the real difference. This workflow strips the noise first, then compares by JSON path.
The jq and diff commands were checked with jq 1.7.1. The JSON examples are intentionally small.
1. Capture both responses
Keep the raw responses. Do not compare a copied browser preview with a terminal output; whitespace and truncation differ.
curl -sS -H "Authorization: Bearer $STAGING_TOKEN" \
"$STAGING_URL/api/orders/42" > staging.json
curl -sS -H "Authorization: Bearer $PROD_TOKEN" \
"$PRODUCTION_URL/api/orders/42" > production.jsonCheck the status code and Content-Type before comparing. If one side returned an HTML error page, a JSON diff is meaningless; see how to debug API responses.
2. Remove fields that always change
Sort keys and delete request-specific fields at every depth with jq:
jq -S 'walk(if type == "object" then del(.requestId, .updatedAt) else . end)' \
staging.json > staging.norm.json
jq -S 'walk(if type == "object" then del(.requestId, .updatedAt) else . end)' \
production.json > production.norm.jsonReplace the field names with the ones your API actually changes on every request. Common examples are requestId, traceId, serverTime and etag. Do not remove a field just because it differs; keep anything that could be the bug.
3. A text diff finds noise, not structure
For these two normalized responses:
{
"data": [
{ "id": 1, "name": "Desk lamp", "price": 24.5, "stock": 3 },
{ "id": 2, "name": "USB-C cable", "price": 5, "stock": 0 }
]
}
{
"data": [
{ "id": 2, "name": "USB-C cable", "price": 5.0, "stock": 0 },
{ "id": 1, "name": "Desk lamp", "price": 24.5, "stock": 4 }
]
}diff -u staging.norm.json production.norm.json exits with status 1 and marks the whole array because the order changed. The real change — data[?(@.id==1)].stock from 3 to 4 — is buried in the reordering.
4. Compare by path with the JSON Diff tool
Paste the normalized responses into the JSON Diff tool, then:
- Choose Arrays: Match by key and enter
idfor arrays of objects. This pairs the twoid: 1items and shows the field-level change. - Choose Ignore order for arrays that have no stable key.
- Add request-specific names to Ignore keys if you did not remove them with
jq.
The report lists paths such as $.data[?(@.id==1)].stock with the old and new values. It also distinguishes a value change (3 → 4) from a type change (5 → "5"). See how to compare two JSON files for the comparison rules.
5. Interpret the result
| Report | Likely cause |
|---|---|
| Added or removed key | Different API version, feature flag or database row. Check the deploy history and migrations. |
| Changed value | Configuration, seed data, user data or a real logic difference. |
| Type changed | Serializer or schema change, such as a number becoming a string. Search the client code for casts. |
| Only array order changed | Missing ORDER BY, or a different query plan. Use Match by key if the API treats the list as a set. |
6. Keep a response pair as a regression test
Save the normalized staging and production responses next to the test that exercises the endpoint. When the backend changes, a failing diff is easier to review than a screenshot. If the API returns large responses, keep one fixture per meaningful case rather than one giant snapshot.
Do not paste secrets into a public diff
Remove access tokens, cookies, email addresses and other personal data before sharing a response. The JSON Diff tool and the JSON Formatter run in your browser, but a screenshot or copied report is still a disclosure. Use redacted fixtures for tickets and pull requests.