How to check if API returns valid JSON

Save the body, then parse it: curl -s -o body.json https://httpbin.org/json followed by node -e "JSON.parse(require('fs').readFileSync('body.json','utf8'))". Silence means the bytes parse. A SyntaxError naming an unexpected < means the endpoint answered with HTML, whatever its content-type header claimed. Repeat on the error path.

Why check this

Clients call response.json() and crash on a body that never parsed. The report arrives as "the page is blank after login", and the cause is a gateway that returned an HTML error page under a JSON content type. Run this on staging sign-off and after any proxy, WAF or ingress change, on the success path and on at least one error path.

Prerequisites

const http = require('node:http');
const page = '<html>\n<head><title>502 Bad Gateway</title></head>\n<body><h1>502 Bad Gateway</h1><hr>nginx</body>\n</html>\n';
http.createServer((req, res) => {
  res.writeHead(502, { 'content-type': 'application/json' });
  res.end(page);
}).listen(8081, '127.0.0.1', () => console.log('listening on 127.0.0.1:8081'));

Stop it with Ctrl+C when the run is over.

Steps

  1. Step 1.

    Capture the body and print the three facts that decide how to read it: status, media type, byte count.

    curl -s -o body.json -w '%{http_code} %{content_type} %{size_download}\n' https://httpbin.org/json
    
    200 application/json 429
  2. Step 2.

    Parse the saved file.

    node -e "JSON.parse(require('fs').readFileSync('body.json','utf8')); console.log('valid JSON')"
    
    valid JSON
  3. Step 3.

    Point the same capture at the local gateway target and read the header it sends.

    curl -s -o bad.json -w '%{http_code} %{content_type} %{size_download}\n' http://127.0.0.1:8081/orders/1
    
    502 application/json 106

    The media type says JSON. Nothing so far contradicts it.

  4. Step 4.

    Parse that body and read the error.

    node -e "JSON.parse(require('fs').readFileSync('bad.json','utf8'))"
    
    SyntaxError: Unexpected token '<', "<html>
    <he"... is not valid JSON

    This is the same message a front end prints in the browser console, which is why testers recognise it faster than they recognise the 502 behind it.

  5. Step 5.

    Turn the check into one pipe that a pipeline can fail on. Point it at a host that answers HTML.

    curl -s https://example.com/ | node -e "let b='';process.stdin.on('data',c=>b+=c).on('end',()=>{try{JSON.parse(b);console.log('valid JSON, '+b.length+' bytes')}catch(e){console.error('NOT JSON: '+e.message);process.exit(1)}})"
    
    NOT JSON: Unexpected token '<', "<!doctype "... is not valid JSON
  6. Step 6.

    Run the same pipe against the JSON endpoint to confirm the guard passes real traffic.

    curl -s https://httpbin.org/json | node -e "let b='';process.stdin.on('data',c=>b+=c).on('end',()=>{try{JSON.parse(b);console.log('valid JSON, '+b.length+' bytes')}catch(e){console.error('NOT JSON: '+e.message);process.exit(1)}})"
    
    valid JSON, 429 bytes

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | valid JSON and a byte count | The body parses on its own | Move on to the schema, which is the next contract. | | Unexpected token '<' | The first byte is a tag, so the body is HTML or XML | Read the status code. A gateway, not the service, wrote this body. | | Unexpected end of JSON input | The body was cut off | Compare size_download with content-length. A truncated response is a transport fault. | | Unexpected non-whitespace character after JSON | Two documents in one body | Usually a stray debug print, or an NDJSON stream read as one object. | | Status 200 with a zero byte count | Empty body sold as success | A 204 belongs here. An empty 200 breaks clients that always parse. |

Common mistakes

Sign: The content-type header says application/json and the client still fails to parse.Cause: The header is set by the route handler, the body by whatever answered instead. Step 3 shows a 502 with a JSON media type and an HTML body. Trusting the header is why this bug reaches production.
Sign: The success path is checked and the endpoint breaks in the field.Cause: Error paths are usually rendered by a different layer: the framework error page, the load balancer, the WAF. Send a request that 4xx or 5xx on purpose and parse that body too.
Sign: A browser shows the JSON fine, the script says it is invalid.Cause: DevTools renders a pretty preview even for a body that failed to parse, and it also shows the response of a redirect target rather than the redirect. Parse the bytes curl saved, not the rendering.
Sign: jq prints the object, so the body is declared valid.Cause: jq accepts a stream of concatenated values and trailing newlines that a strict parser rejects. Use the parser the client uses, which for a web front end is JSON.parse.

What to check next

FAQ

How to validate JSON format without installing anything?

Python is on most build agents: curl -s URL | python -c "import json,sys;json.load(sys.stdin)". It prints nothing on success and raises json.decoder.JSONDecodeError with a line and column on failure. The exit code is 1, so a pipeline can gate on it.

How to check if a response is valid JSON in the browser?

Open DevTools, Network tab, click the request, then the Response tab, which shows raw bytes. The Preview tab renders a tree even when parsing failed, so the raw view is the one that answers the question.

Does a 200 status mean the body is valid JSON?

No. Status and body are set independently, and a proxy can rewrite one without the other. Step 3 returns a 502 with a JSON media type and an HTML body, which is the same decoupling seen from the other direction.

Should an empty body count as valid JSON?

No. An empty string is not a JSON value, and JSON.parse('') throws. An endpoint with nothing to say should answer 204 No Content and set no content type, so the client skips parsing instead of failing at it.

Verified

Verified by Maks Vernycurl 8.21.0node 22.23.2

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

basic5 minpublished updated Maks Verny