</>CodeScrub

How to Read and Debug API JSON Responses

A practical guide to reading messy API responses, spotting common issues, and extracting the data you need. Includes curl tips, browser DevTools tricks, and formatting tools.

8 min read
  • json
  • api
  • debugging
  • devtools
  • curl

title: "How to Read and Debug API JSON Responses" description: "A practical guide to reading messy API responses, spotting common issues, and extracting the data you need. Includes curl tips, browser DevTools tricks, and formatting tools." date: "2026-06-10" tags: ["json", "api", "debugging", "devtools", "curl"] relatedTool: "json-formatter" published: true

You hit an API endpoint. You get back a wall of minified JSON. You need to find one value buried three levels deep in a nested response, figure out why your client code is exploding, and get on with your day. This is the workflow for reading, formatting, and debugging API responses without losing your mind — from raw response to extracted value in a few minutes.

Step 1: Get the Raw Response

Three ways to grab the response, in order of how often you'll use them:

curl (the command-line default):

curl -s https://api.example.com/users/1 | jq .

The -s flag silences curl's progress bar so it doesn't pollute the JSON with a status line, and piping to jq . pretty-prints the output. If you don't have jq installed, python3 -m json.tool is a decent fallback that ships with every Python 3 installation. Add -i to see the response headers alongside the body — often more useful than the body itself when you're trying to figure out why the API responded the way it did.

Browser DevTools (when the request comes from your web app):

  1. Open DevTools (F12) → Network tab.
  2. Trigger the request by refreshing the page or clicking whatever fires it.
  3. Click the request in the list, then open the Preview tab for a formatted tree view, or Response for the raw JSON string.
  4. Right-click the request → CopyCopy response to grab the raw JSON, or Copy as cURL to reproduce the exact call in your terminal.

The Preview tab in Chrome and Edge is often good enough that you don't need to copy anything — it renders nested objects as collapsible trees. Firefox's equivalent is a hair worse for large payloads but has better search.

Postman / Insomnia / Bruno (when you're iterating on request shape):

Send the request, and the response pane formats the JSON automatically with syntax highlighting and collapsible sections. Click "Raw" to see the unformatted version if you need to inspect exact bytes. All three tools also save the request for later, which is what makes them worth the extra window.

Step 2: Format It

For a quick standalone paste, drop the raw response into the CodeScrub JSON Formatter — indented output, syntax highlighting, error position callouts, all in the browser with no upload. It makes deeply nested structures visible at a glance and catches syntax errors when the response isn't quite valid JSON (which happens more often than API vendors admit).

Step 3: Navigate the Structure

Once you can see the response, the next problem is extracting the specific value you actually care about. Consider this response shape:

{
  "data": {
    "user": {
      "id": 42,
      "profile": {
        "name": "Ada Lovelace",
        "emails": [
          { "type": "work", "address": "ada@company.com" },
          { "type": "personal", "address": "ada@home.com" }
        ]
      }
    }
  },
  "meta": {
    "status": 200,
    "timestamp": "2026-05-28T14:30:00Z"
  }
}

To get Ada's work email, the path is data.user.profile.emails[0].address. In every mainstream language, that path translates directly into an access expression:

JavaScript / TypeScript:

const email = response.data.user.profile.emails[0].address;

Python:

email = response["data"]["user"]["profile"]["emails"][0]["address"]

jq (from the command line):

curl -s URL | jq '.data.user.profile.emails[0].address'

Two things to know about these paths in production code. First, dot access can throw if any intermediate value is missing — use optional chaining (response?.data?.user?.profile?.emails?.[0]?.address in JS/TS, or .get() with defaults in Python) whenever the shape isn't guaranteed. Second, when the array index isn't fixed — you want the work email, not "the first email" — filter by the field: jq '.data.user.profile.emails[] | select(.type=="work") | .address'. Positional access is fragile the moment the API reorders records.

Common API Response Issues

Five failure modes you'll see over and over, and how to diagnose each one at a glance.

1. Response is HTML instead of JSON.

Symptom: SyntaxError: Unexpected token < when you try to parse it.

Cause: You're hitting a web page or an error page, not the API endpoint. Common triggers: missing /api/ in the URL, an unauthenticated request being redirected to a login page, or a proxy returning its own error HTML.

Fix: Look at the Content-Type response header. It should be application/json. If it's text/html, the URL is wrong or a middleware is intercepting the request before it reaches the API. Print the first 200 characters of the response body — nine times out of ten it starts with <!DOCTYPE html> and the URL problem is obvious.

2. Empty response or null.

Symptom: API returns {}, [], null, or {"data": null}.

Cause: The resource doesn't exist, or your token doesn't have permission to see it. Some APIs prefer to return a "polite" empty response rather than an honest 404 or 403, which makes debugging harder than it should be.

Fix: Check the HTTP status code first — an empty body with a 200 is fundamentally different from a 404 (missing resource) or 403 (unauthorized). Also verify that the ID, filter, or query parameters you're using actually match data — a typo in a filter that returns "no results" looks identical to a permissions problem in the body alone.

3. Deeply nested data.

Symptom: The value you need is five levels deep in something like data.attributes.relationships.included.

Cause: API design choices, especially JSON:API-style responses that wrap everything in structural metadata layers.

Fix: Don't fight it — extract just the path you need at the boundary of your application and normalize into your own flat shape. jq '.data.attributes' pulls the useful layer out; a small mapResponse() function in your client turns the API's shape into whatever your code actually wants to work with. Do the mapping in one place so the rest of your codebase never sees the wrapper.

4. Inconsistent types across records.

Symptom: A field is sometimes a string, sometimes a number, sometimes null, sometimes missing entirely.

Cause: Weakly typed API, data-quality issues, or fields that mean different things depending on other fields in the record.

Fix: Always check for null before accessing nested properties (optional chaining in JS, .get() chains in Python). For type inconsistency, coerce explicitly at the boundary — Number(response.count) || 0, str(response.id). If the type coercion is complex enough, validate the response with a schema library (Zod, Pydantic, JSON Schema) and reject unexpected shapes rather than silently guessing.

5. Pagination cutting off results.

Symptom: You expected 500 rows and got 25 (or 100, or 1,000 — always a round number that's clearly a default limit).

Cause: The API paginates by default and you didn't ask for the next page.

Fix: Look for next, cursor, offset, page, or has_more fields somewhere in the response — often under meta, pagination, or a Link header. Loop until you've exhausted the pages. Common patterns are cursor-based (pass cursor from the previous response's next_cursor), page-based (increment page), or link-header based (follow the Link: <...>; rel="next" header until it disappears).

Useful jq Commands for API Debugging

The jq commands you'll copy-paste most often. Bookmark this section.

# Pretty print
curl -s URL | jq .

# Extract one field
curl -s URL | jq '.data.name'

# Get all names from an array
curl -s URL | jq '.users[].name'

# Filter array items by a condition
curl -s URL | jq '.users[] | select(.active == true)'

# Count items
curl -s URL | jq '.users | length'

# Get the top-level keys of an object
curl -s URL | jq '.data | keys'

# Pick multiple fields into a new object
curl -s URL | jq '.users[] | {id, name, email}'

# Get unique values of a field
curl -s URL | jq '[.users[].role] | unique'

# Sort by a field
curl -s URL | jq '.users | sort_by(.created_at)'

# Save to a file
curl -s URL | jq . > response.json

Combine them freely with pipes — jq was designed for it. jq '.users[] | select(.active) | {id, name}' gives you the id and name of every active user in one line.

When the Response Isn't Valid JSON

Sometimes APIs return almost valid JSON — a trailing comma the server didn't clean up, single quotes from a debugging log, // comments left by a developer, or a UTF-8 BOM at the start of the file that breaks strict parsers. Paste the response into the CodeScrub JSON Formatter to see exactly where the error is, with the line and character position callout. Common culprits worth checking specifically: the response might be JSONP (JSON wrapped in a callback function like myCallback({...})), or start with a security prefix like )]}'\n that some frameworks (older Angular, some Google APIs) prepend to defeat JSON-hijacking attacks. In both cases, strip the wrapping bytes and the inner content parses cleanly.

Bottom line

Most API debugging is really just JSON formatting plus path navigation. Get the raw response with curl or DevTools, format it so you can see the structure, walk the path to the value you need, and if any of it looks off, work through the five common failure modes above before assuming something exotic is wrong. Nine times out of ten it's one of those five.

Ad space · blog-article-mid