JSON to Chart
Paste an API response or drop a json, jsonl or ndjson file and this page flattens it into a rectangle, then charts it. Nested objects turn into dotted column names, a records array inside a wrapper object is found and unwrapped, and from there it behaves exactly like the CSV chart tool. Nothing is uploaded.
Nothing to paste? Try the sample campaign data on csv-to-chart · the sample here is a CSV, and the engine is the same one.
Want the flat table rather than a picture? json-to-csv
A chart needs a rectangle, so first the JSON becomes one
Charts are made of rows and columns. JSON is a tree. The gap between those two facts is the entire content of this page, and the way it is closed is with the same flattener that powers json-to-csv and the other JSON converters here, so the columns you see in a chart are the same columns you would get in a download. One implementation, one behavior, no surprises when you move between tools.
Nested objects are flattened by joining the key path with dots. customer.name, customer.plan.tier, metrics.latency.p95. Four levels deep is the limit, which comfortably covers real payloads while keeping the column list something a human can scan. Anything deeper is kept as its raw JSON text in one cell rather than dropped, and the panel says so. Arrays inside a record are also left in their cell instead of being exploded into extra rows, because multiplying your row count changes every sum on the chart, and a tool should not do that on a guess.
Four input shapes are recognized. A bare array of objects is the simplest and charts directly. A single object becomes a one-row table. An object with exactly one array property inside it, which is what almost every paginated API returns as data or results or items, gets unwrapped so that array becomes the rows, and the panel names the key it used and mentions the sibling fields it set aside. And JSON Lines, one value per line, is detected when whole-document parsing fails.
After flattening, it is the CSV chart tool
Once the rectangle exists, nothing here knows or cares that it came from JSON. The same columns get the same roles, the same default chart is chosen, the same controls apply.
- Chart. Bar, column, line, area, donut, scatter, histogram and boxplot. Switching redraws from parsed rows and never re-reads the input.
- Measure. The column being aggregated, or Count rows when the count is the answer, which is often the case for a page of log records.
- Dimension. The column being grouped by. Pick a dotted name like
customer.plan.tierand the chart groups by the nested field directly. - Y measure. Shown only for scatter.
- Aggregate. Sum, average, count, count distinct, minimum, maximum, median.
- Show and Sort. Top 5 through everything, ordered by value, by label, or left in the order the records arrived.
- Time grain. Auto, day, week, month, quarter, year. ISO timestamps, which is what JSON almost always carries, are recognized as dates and get a real time axis.
Worked example: an API response, pasted
This came out of a billing endpoint and went straight from a terminal into the paste box:
{
"page": 1,
"has_more": true,
"data": [
{
"id": "sub_9f21",
"created": "2024-03-02T09:14:00Z",
"amount": 4900,
"currency": "usd",
"customer": { "name": "Northwind Ltd", "region": "EMEA",
"plan": { "tier": "growth", "seats": 24 } },
"tags": ["annual", "referred"]
},
{
"id": "sub_a103",
"created": "2024-03-02T11:47:00Z",
"amount": 12900,
"currency": "usd",
"customer": { "name": "Ridgeline", "region": "AMER",
"plan": { "tier": "scale", "seats": 61 } },
"tags": []
}
]
}
The wrapper object has one array property, data, so that array becomes the rows and a note appears reading Read the records from "data". Two sibling fields were left out. The flattened columns are id, created, amount, currency, customer.name, customer.region, customer.plan.tier, customer.plan.seats and tags. The tags array stays in its cell as text, and no row was invented for the second tag on the first record.
With nothing clicked, the panel draws a line titled amount over time, captioned Sum of amount · daily totals · 1 point, with a TREND kicker. On two records that is a thin chart, so set Chart to Bar and Dimension to customer.plan.tier. The title becomes amount by customer.plan.tier, the kicker becomes BREAKDOWN, and two horizontal bars appear labeled scale and growth with 12,900 and 4,900 written beside them in mono.
One thing to notice, because it will bite somebody: the values are in cents, so the axis reads 12,900 rather than $129.00. The column has no currency symbol in it, so nothing here can know it is money. That is not a flaw in the parser, it is a property of the API, and the fix is to divide by a hundred in the app before charting. Download chart data gives you the two aggregated rows exactly as drawn.
Ragged records, and why your column list is long
A CSV has a header, so every row has the same fields whether or not they hold anything. JSON has no such constraint, and real payloads exploit that constantly. An error object appears on the two percent of records that failed. A promo block exists only for orders that used a code. A debug field shows up when a request was made with a flag set. Every one of those becomes a column, because the alternative is deciding on your behalf that a rare field does not matter, and rare fields are frequently the interesting ones.
The consequence is a column list longer than you expected, most of it nearly empty. Do not fight it. The Measure and Dimension dropdowns list every column, and you only ever pick two or three. If the sprawl is genuinely in the way, trim the payload before pasting it, either with a select in your query or with a jq filter on the way out of curl. Ten seconds there saves scrolling here.
Deep nesting has a similar shape of problem. Four levels of dotted path is the flattening limit, and a payload nested six deep will have its remainder stored as raw JSON text in a cell. That cell is not a number and not a category worth grouping by, so it is not useful to chart, but it is visible, which is the point. Nothing disappears without a note. If the interesting numbers live at level six, extract that subtree and chart it on its own, which is usually the better analysis anyway.
Four exports
- Download PNG. Two-times density on the panel background, ready for a slide without becoming a black box on a dark theme.
- Copy image. The same PNG on the clipboard, which is how a chart usually ends up in an incident channel thirty seconds after the response was pasted.
- Download SVG. Vectors with real paths and real text, sharp at any size and editable in Figma or Illustrator.
- Download chart data. The aggregated numbers as CSV, one row per bar, point or slice. This is the flattened, aggregated truth behind the picture, and it is the fastest way to check the arithmetic yourself.
For the whole flattened table rather than the aggregate, json-to-csv and json-to-excel use the identical flattener, so the columns will match what you saw here.
What this page will not do
- It will not fetch a URL. Paste the response, do not paste the endpoint. A page that fetched on your behalf would need your credentials, and nothing here should ever ask for those.
- No array explosion. An array inside a record stays in its cell. One record is one row, always.
- Four levels of nesting. Deeper paths are kept as raw JSON text in a cell rather than becoming columns.
- No JSONPath or jq expression. There is no query box for selecting a subtree. Extract it before pasting.
- No unit inference. Cents stay cents, milliseconds stay milliseconds. Nothing in the payload says otherwise, so nothing here guesses.
- No filtering. Every record is charted. Filter in the app or in your query.
- One chart at a time. For a KPI strip and several panels from one payload, use the dashboard builder.
Frequently Asked Questions
What JSON shapes does this accept?
Four of them, and they cover essentially everything people paste. An array of objects charts directly, one object per row. A single bare object becomes a one-row table, which is rarely a useful chart but is never an error. An object with one array property inside it, which is what almost every API returns, is unwrapped so the array becomes the rows, and the panel says which key it used. And JSON Lines, meaning one JSON value per line with no wrapping array, is detected when whole-document parsing fails and read line by line.
How do nested objects become columns?
By joining the key path with dots. A record holding a customer object with a name inside it produces a column called customer.name, and a plan nested under that customer becomes customer.plan.tier. Nesting is followed to four levels, which is deep enough for real API payloads and shallow enough that the column list stays readable. Anything deeper than that is kept as its raw JSON text in a single cell rather than being dropped, so nothing goes missing quietly, and the panel tells you it happened.
What happens to arrays inside a record?
They are not exploded into extra rows. If a record holds an array of tags or an array of line items, that array stays inside its own cell rather than silently multiplying your row count, because inventing rows changes every total on the chart and does it without asking. If what you actually want is one row per line item, point the tool at that inner array instead: extract it, paste it on its own, and chart that. The flattener is deliberately conservative here and I would rather explain the behavior than guess at your intent.
Why are so many of my columns nearly empty?
Because the records are ragged, which is normal in JSON and impossible in CSV. Every distinct key path found anywhere in the data becomes a column, so if one record in five thousand carries a debug object with nine fields in it, you get nine columns that are blank in 4,999 rows. That is not a bug and it is not something the tool should hide by dropping columns you did not ask it to drop. Pick the columns you actually want in the Measure and Dimension dropdowns and ignore the rest, or trim the payload before pasting it.
Can I paste straight from curl or a browser devtools panel?
Yes, and it is the most common way this page gets used. Run your request, copy the response body, paste it into the paste tab, and a chart appears. Pretty-printed and minified JSON both work, whitespace is irrelevant, and a byte order mark at the front is stripped. Trailing commas and unquoted keys, which a hand-edited config often has, are read with a relaxed parser and reported in a note rather than rejected outright. If the paste is truncated, which happens when a terminal has cut the output off, the error names the line to look at rather than a character offset nobody can find.
Does anything get sent to a server?
No. The parsing, the flattening, the aggregation and the drawing all happen in your browser tab. There is no endpoint behind this page and no request carrying your payload anywhere. That matters more here than on most pages, because API responses tend to contain customer names, email addresses and internal identifiers, and pasting one into a hosted tool means handing all of it to somebody else. Close the tab and the data is gone.
Are JSONL and NDJSON supported?
Yes, both, and they are the same format under two names: one JSON value per line, no wrapping array, no commas between records. It is what log pipelines and bulk export endpoints emit. The reader tries whole-document parsing first, and when that fails it reads line by line instead and says so in a note. A file that is mostly valid JSONL with one broken line gets an error naming that line number, because a character offset into a fifty megabyte file is a useless thing to tell somebody.
What happens after the JSON becomes a table?
Nothing on this page is JSON-specific from that point onward. The flattened rectangle goes through exactly the same code as a CSV: each column gets a role, a default chart is chosen from those roles, and the same seven shapes, the same aggregates and the same PNG, SVG, copy-image and chart-data exports are available. A JSON payload and a CSV holding the same numbers produce identical charts, which is deliberate rather than accidental.
Related
Paste the response
Copy it out of the terminal, drop it in, take the PNG. No sign-up, no upload, no row cap.
Back to the chart maker