How to Debug REST API JSON Responses: A Developer's Complete Guide
API returning broken JSON? Find the exact error fast: inspect raw responses with curl, DevTools, and Python — then repair it in one click. Free tool included.
Have broken JSON right now? Fix it free in under 1 second — no signup.
Fix My JSON →When an API starts returning broken JSON, your application crashes, your users hit errors, and you have a debugging session ahead of you. Whether you're consuming a third-party API or troubleshooting your own, the process is the same: inspect what's actually coming back over the wire, identify where the JSON breaks, and fix it at the source — or handle it defensively on your end.
This guide walks through every tool and technique you need, from raw curl requests to production logging strategies.
Why REST APIs Return Broken JSON
Before reaching for debugging tools, it helps to understand the common causes:
1. Content-type mismatch. The API returnstext/html (often an error page) but your client tries to parse it as JSON. This is the single most common cause of "unexpected token '<'" errors — the < is the opening tag.
2. Encoding issues. BOM characters, non-UTF-8 byte sequences, or null bytes in field values can corrupt a JSON parser even when the structure is syntactically valid.
3. Truncated responses. Network timeouts, proxy limits, or streaming APIs that get cut off mid-response produce incomplete JSON — missing closing brackets or cut-off string values.
4. Server-side template injection. Some backend frameworks accidentally render partial HTML into a JSON template, producing strings like {"message": "Success
"}.
5. Double-encoded or escaped JSON. A JSON string containing another JSON object that was stringified twice: "{\"name\":\"Alice\"}" when you expected {"name":"Alice"}.
6. Gzip compression not decoded. If your HTTP client doesn't handle Content-Encoding: gzip automatically, you get binary garbage instead of text.
Tool 1: curl — See the Raw Response
curl is your first stop. It shows you exactly what the server sends before any client-side processing.
# Basic GET request — see headers and body
curl -i https://api.example.com/users/1
POST with JSON body
curl -i -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{"query": "test"}' \
https://api.example.com/search
Save response body to file for closer inspection
curl -s https://api.example.com/data > response.json
Pipe to jq for pretty-printing (fails loudly on invalid JSON)
curl -s https://api.example.com/data | jq .
Check only the Content-Type header
curl -sI https://api.example.com/data | grep -i content-type
The -i flag includes response headers. Always check Content-Type — if it says text/html when you expected application/json, the body is an error page, not data.
Tool 2: HTTPie — Human-Friendly CLI
HTTPie is a modern alternative to curl with coloured output and automatic JSON formatting:
# Install
pip install httpie
Simple GET — auto-formats JSON
http GET https://api.example.com/users/1
POST JSON
http POST https://api.example.com/search query=test Authorization:"Bearer TOKEN"
Show full request + response
http --all GET https://api.example.com/users/1
HTTPie will show you clearly if the response is not valid JSON — it falls back to raw text display rather than crashing.
Tool 3: Browser DevTools Network Tab
For browser-based API calls, DevTools is indispensable:
- Open DevTools (F12) and click the Network tab
- Filter by Fetch/XHR to see only API calls
- Click any request and check the Headers tab for
Content-Type - Click the Response tab for the raw body
- Click the Preview tab — if it shows a parse error or raw text where you expect a tree, the JSON is broken
Key things to look for in the Preview tab:
- A rendered HTML page (proxy error, auth redirect, 502 page)
- Raw text with visible corruption characters
- A truncated JSON structure (ends mid-value)
Tool 4: Postman
Postman provides the most complete debugging environment:
- Tests tab: Write assertions against the response —
pm.response.json()throws if the body is not valid JSON - Console (View > Show Postman Console): Shows raw request and response bytes including headers
- Visualize tab: Custom renderers for complex response shapes
Quick Postman test to validate JSON structure:
pm.test("Response is valid JSON", function () {
pm.response.to.be.json;
const body = pm.response.json();
pm.expect(body).to.have.property("data");
});
Reading HTTP Headers for JSON Content-Type
The Content-Type response header tells you what the server claims to be sending:
Content-Type: application/json; charset=utf-8 ✅ Expected
Content-Type: text/html; charset=utf-8 ❌ Error page
Content-Type: text/plain ❌ Not parsed as JSON
Content-Type: application/octet-stream ❌ Binary
Even with the correct Content-Type, the body can still be broken — but a wrong Content-Type is a near-certain sign something went wrong upstream (auth failure, WAF block, maintenance page, load balancer 502).
Programmatic Validation
Node.js
async function fetchJson(url, options = {}) {
const response = await fetch(url, options);
const contentType = response.headers.get("content-type") || "";
if (!contentType.includes("application/json")) {
const text = await response.text();
throw new Error(
Expected JSON but got ${contentType}. Body preview: ${text.slice(0, 200)}
);
}
let text;
try {
text = await response.text();
return JSON.parse(text);
} catch (err) {
throw new Error(
JSON parse failed: ${err.message}. Body preview: ${text?.slice(0, 200)}
);
}
}
Python
import requests
import json
def fetch_json(url: str, **kwargs) -> dict:
response = requests.get(url, **kwargs)
content_type = response.headers.get("Content-Type", "")
if "application/json" not in content_type:
raise ValueError(
f"Expected application/json, got {content_type}. "
f"Body preview: {response.text[:200]}"
)
try:
return response.json()
except json.JSONDecodeError as e:
raise ValueError(
f"JSON parse failed at position {e.pos}: {e.msg}. "
f"Body preview: {response.text[:200]}"
) from e
curl Pipeline for CI/CD
#!/bin/bash
RESPONSE=$(curl -sf -H "Accept: application/json" https://api.example.com/health)
STATUS=$?
if [ $STATUS -ne 0 ]; then
echo "Request failed with status $STATUS"
exit 1
fi
Validate JSON with jq
echo "$RESPONSE" | jq . > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Invalid JSON response: $RESPONSE"
exit 1
fi
echo "API health check passed"
Handling HTML Error Responses (502/503)
The most common real-world scenario: your API is behind a load balancer or reverse proxy. When the upstream server is down, the proxy returns an HTML error page with a 200 OK or 5xx status code, and your JSON client tries to parse .
Defensive pattern — always check status code and content-type before parsing:
async function safeApiCall(url) {
const res = await fetch(url);
// Treat any non-2xx as an error before touching the body
if (!res.ok) {
const body = await res.text();
throw new Error(HTTP ${res.status}: ${body.slice(0, 300)});
}
const ct = res.headers.get("content-type") || "";
if (!ct.includes("json")) {
const body = await res.text();
throw new Error(Unexpected content-type "${ct}": ${body.slice(0, 300)});
}
return res.json();
}
Production Logging Strategies
Logging broken JSON in production requires care — you want enough context to reproduce the problem without logging sensitive user data.
Log the first N bytes on parse failure:import logging
import json
logger = logging.getLogger(__name__)
def parse_api_response(raw: str, endpoint: str) -> dict:
try:
return json.loads(raw)
except json.JSONDecodeError as e:
logger.error(
"JSON parse failure",
extra={
"endpoint": endpoint,
"error": str(e),
"error_position": e.pos,
"body_length": len(raw),
# Log a safe window around the error
"body_window": raw[max(0, e.pos - 50): e.pos + 50],
}
)
raise
Structured logging with correlation IDs:
function parseWithLogging(text, requestId, endpoint) {
try {
return JSON.parse(text);
} catch (err) {
logger.error({
event: "json_parse_failure",
requestId,
endpoint,
errorMessage: err.message,
bodyLength: text.length,
bodyPreview: text.slice(0, 100).replace(/\n/g, "\\n"),
});
throw err;
}
}
Using JSON Validator and JSON Fixer in Your Dev Workflow
When you have a broken response body saved to a file or clipboard, the fastest path to understanding why it's broken is a purpose-built tool:
- Paste the raw response into the JSON Validator — it highlights exactly which line and character caused the parse failure, with a plain-English description of the error
- If the JSON is repairable (trailing commas, single quotes, Python literals, markdown fences), paste it into AI JSONMedic — it repairs the JSON automatically and shows a diff of every change made
- Use the JSON Formatter to pretty-print minified API responses before reading them
These tools are especially useful when debugging third-party API responses that you can't modify at the source — you can use the repaired JSON to understand the intended structure and then add defensive parsing in your client code.
Testing API JSON with Jest and pytest
Jest (Node.js)
// api.test.js
import { fetchJson } from "./api";
describe("GET /users/:id", () => {
it("returns valid user object", async () => {
const user = await fetchJson("https://api.example.com/users/1");
expect(user).toMatchObject({
id: expect.any(Number),
name: expect.any(String),
email: expect.stringMatching(/@/),
});
});
it("throws on non-JSON response", async () => {
// Mock a bad response
global.fetch = jest.fn().mockResolvedValue({
ok: true,
headers: new Headers({ "content-type": "text/html" }),
text: async () => "<html>Error</html>",
});
await expect(fetchJson("/bad-endpoint")).rejects.toThrow("Expected JSON");
});
});
pytest
# test_api.py
import pytest
import responses
import requests
from myapp.api import fetch_json
@responses.activate
def test_returns_valid_json():
responses.add(
responses.GET,
"https://api.example.com/users/1",
json={"id": 1, "name": "Alice"},
status=200,
)
result = fetch_json("https://api.example.com/users/1")
assert result["id"] == 1
assert isinstance(result["name"], str)
@responses.activate
def test_raises_on_html_response():
responses.add(
responses.GET,
"https://api.example.com/bad",
body="<html>502 Bad Gateway</html>",
content_type="text/html",
status=502,
)
with pytest.raises(ValueError, match="Expected application/json"):
fetch_json("https://api.example.com/bad")
Summary: Debugging Checklist
When an API returns broken JSON, work through this list in order:
- Check the HTTP status code — non-2xx almost always means an error page, not data
- Check
Content-Type—text/htmlmeans you're parsing an error page - Inspect the raw body with
curl -ior DevTools Network > Response tab - Check response length — a suspiciously small body is often a truncated response
- Look for BOM or binary characters at the start of the body
- Validate the JSON structure using the JSON Validator for a precise error location
- Attempt automated repair using AI JSONMedic if the source is a third-party API you can't fix
- Add defensive error handling in your client so future failures produce actionable log messages
Broken JSON in production APIs is almost always traceable to one of a handful of root causes. The tooling to find it fast is already on your machine — you just need to know where to look.
For a reference on specific JSON error messages and what they mean, see JSON Syntax Errors Explained. If the broken JSON is coming from an AI or LLM response, the LLM JSON Repair Guide covers the additional failure patterns unique to AI-generated output. For automated repair in production, the JSON Repair API lets you send broken JSON and get fixed JSON back — no auth, no setup, CORS enabled.
FAQ
How do I find which field is causing a JSON parse error in an API response?
Use a JSON validator with field-level error reporting — paste the response into the JSON Validator to get the exact line and column of the error. In code, wrap JSON.parse() in a try/catch and log the error message, which includes character position. For large responses, bisect: remove the second half and try parsing; repeat until you isolate the bad section.
What is the fastest way to inspect a broken API JSON response?
Copy the raw response body (in DevTools: Network tab → select request → Response tab → Copy Response Body, or in Postman: Raw view → Copy) and paste it into AI JSONMedic. It identifies every issue in the JSON, shows what was wrong, and repairs it — giving you a clean version to work with while you fix the API.
Why does my API return valid JSON in development but broken JSON in production?
This usually traces to environment differences: different character encoding in prod (BOM characters, Latin-1 vs UTF-8), content negotiation returning a different format, caching middleware adding or modifying the response, or the prod database returning different data types (null vs empty string, int vs string). Log the raw Content-Type header and first 20 bytes of the response in both environments and compare.
How do I add JSON logging to an Express.js API for debugging?
Add a response interceptor that logs the stringified body before sending: use res.json = function(body) { console.log('response:', JSON.stringify(body).slice(0, 500)); return originalJson.call(this, body); }. For production, use a structured logging library (pino, winston) with serializers that log request/response bodies at debug level only.
What should I check when an API returns a 200 status but the body won't parse as JSON?
Check in this order: (1) Content-Type header — should be application/json; (2) Response body in Raw view — look for HTML at the start (CDN error page); (3) First bytes — BOM character (EF BB BF) invisible in most UIs; (4) Response body length vs Content-Length header — truncation causes parse failure; (5) Transfer-Encoding: chunked with a proxy that didn't reassemble chunks correctly.
Still dealing with broken JSON?
Paste it in and get it fixed in under 1 second — free, no signup, no install. Works with ChatGPT, Claude, n8n, and any AI output.
Fix My JSON Free →Related Articles