Why an offline JSON tool matters now
In November 2025 the security research firm watchTowr Labs published findings about two of the most widely used online code-formatting utilities. Both offered a “save” or “share” feature that stores a submission on the server and hands back a short link. The links were generated from a predictable, enumerable identifier — so anyone willing to count upwards could walk the entire archive, including submissions the person who pasted them had never intended to publish.
The researchers reported recovering more than 80,000 stored files, over 5 GB of data accumulated across roughly five years. The contents were exactly what you would expect developers to paste into a formatter while debugging, and exactly what you would least like to find in a public bucket:
- Active Directory credentials and domain administrator passwords.
- Cloud provider access keys and API tokens.
- Database connection strings, complete with hostnames and passwords.
- Private keys, session tokens and CI/CD secrets.
- Personal data belonging to customers of banks, government departments, healthcare providers, universities and — with a certain grim irony — cybersecurity companies.
To test whether anyone was actually harvesting the archive, the researchers planted canary credentials: realistic-looking but fake secrets that would report back if used. They were exercised within days. This was not a theoretical exposure of dormant data; it was a live, monitored supply of working credentials.
None of this required a breach. No attacker compromised a server. The data was simply stored, given a guessable address, and left reachable. The failure was architectural: the tool had somewhere to put your data, so eventually your data was there.
The structural fix is not a better privacy policy. It is having nowhere to store the data in the first place. This page has no upload endpoint, no database and no share link, so there is no archive of pasted JSON that could later be enumerated, subpoenaed, sold, or left in a misconfigured bucket. That property comes from the architecture, not from a promise.
Read the reporting yourself
- The Hacker News — coverage of the watchTowr Labs research Summary of the research, with the categories of secret recovered.
- BleepingComputer — how the stored links were enumerated Covers which sectors the affected organisations were in and how the links were enumerated.
- SecurityWeek — “Thousands of secrets leaked on code formatting platforms” Additional detail on the scale and on the canary-credential experiment.
What to do if you have used one of those sites
- Assume anything you pasted into a hosted formatter is public. Not “probably fine” — public.
- Rotate every credential that could have appeared in one of those payloads: database passwords, API keys, service-account tokens, webhook secrets, JWT signing keys.
- Check your provider’s access logs for use of those credentials from unfamiliar addresses, going back as far as your retention allows.
- Change the habit, not just the keys. Redact before you paste, or use a tool that has nowhere to put the data.
The wider lesson is not about two specific websites. It is that a formatter has no technical reason to be a network service at all. Parsing JSON is something every browser has done natively since 2011. A page that sends your document to a server to add whitespace to it has chosen to take custody of your data for no functional benefit.
How it works
Everything on this page is client-side JavaScript, split into two small files with no dependencies between them and nothing fetched from anywhere.
Parsing
Your text goes to the browser’s built-in JSON.parse. That is the same
parser your application uses, written in C++ and shipped with the browser, so a document
this page accepts is a document your code will accept. Nothing here uses
eval or the Function constructor — a JSON tool that
evaluated its input would be a genuine security hole, because a payload could then run
code in your session rather than merely being read.
Explaining the failure
When JSON.parse refuses, its message is famously unhelpful and differs
between engines: Chrome says one thing, Firefox another, Safari a third, and some of them
give you a character offset with no line number. So this page runs a second pass — a
strict RFC 8259 scanner written specifically to find and name the first mistake. It
reports:
- the line and column, converted from the character offset by counting newlines, plus the raw character index;
- the offending line itself, with a caret under the exact character (long lines are windowed around the caret, so a 4 MB single-line document still gives you something readable);
- a named cause where one can be identified — trailing comma,
single-quoted string, unquoted key,
NaN, a Windows path with an unescaped backslash, a byte order mark, a comment, two concatenated documents, an HTML error page arriving where JSON was expected.
The scanner is an explicit state machine with its own stack rather than a recursive
function, so a pathologically nested document produces an error message instead of a
“maximum call stack size exceeded”. It is cross-checked against
JSON.parse on twenty thousand randomly mutated documents; the two agree on
every one about whether the input is valid.
Formatting and minifying
Pretty-printing is JSON.stringify(value, null, indent); minifying is
JSON.stringify(value). Both operate on the parsed value, not on the text, which
is why the output is always canonically formatted and why formatting doubles as a
validation step. Note the consequence: comments, trailing commas and original key order
inside a re-serialised object are not preserved, because they were never part of the
parsed value. Key insertion order is preserved, except that integer-like keys are
hoisted and sorted — that is a rule of the JavaScript object model, not a choice
made here.
The tree
The tree is built lazily. On load it opens about three hundred rows and stops; deeper
nodes are constructed only when you expand them, and any node with more than two hundred
children renders them in pages. That is what keeps a multi-megabyte array from producing
hundreds of thousands of DOM nodes and freezing the tab. Collapsed containers show their
size — {…} 12 keys, […] 340 items — so
you can judge a subtree without opening it.
The tree is a proper ARIA tree widget: arrow keys move and open, ← collapses or jumps to the parent, Enter toggles, Home and End jump to the ends, and * opens all siblings of the focused node.
Escaping
Every fragment of your document that reaches the page is written with
textContent, never by assembling an HTML string. Paste a value containing
<img src=x onerror=alert(1)> and it renders as those literal characters
in the tree, in the output pane and in any error message. For a tool whose entire input is
untrusted text, that is the one bug that would matter most.
Worked examples
1. An API response that will not parse
You call an endpoint, the client throws, and the message is
Unexpected token < in JSON at position 0. Paste the raw response body here
and the answer is immediate:
<!DOCTYPE html>
<html><head><title>502 Bad Gateway</title></head>
The tool reports “This looks like HTML or XML, not JSON” at line 1,
column 1. The endpoint did not return bad JSON; it returned a gateway error page with a
text/html body, and your client tried to parse it anyway. The fix is not in
your parsing code — it is to check response.ok and the
Content-Type header before parsing, and to log the raw body when the check
fails. See the full guide to that error.
2. A config file a human edited
{
// service settings
'host': "db.internal",
port: 5432,
replicas: 3,
}
{
"host": "db.internal",
"port": 5432,
"replicas": 3
}
Four separate violations, and the tool reports them one at a time in the order a
parser meets them: the // comment at line 2 column 3, then the single-quoted
key, then the unquoted port, then the trailing comma after
3. Every one of these is legal in a JavaScript object literal, which is
exactly why they are the four most common mistakes in hand-written JSON. If you need
comments in a config file, you need JSON5, JSONC or YAML — not JSON.
3. Finding one record inside a large payload
A 6 MB export from an analytics API, one enormous array of event objects. Reading
it as text is hopeless. Paste it, press Format, switch to
Tree, and the root shows […] 41,208 items without
having rendered any of them.
Type checkout_failed into the filter. The search walks the parsed value,
reports how many keys and values contain that text, and lists the first two hundred with
their full paths — $[18422].event.name. Click one and the tree opens
every ancestor down to that node, paging through the array in blocks of two hundred until
it reaches index 18,422, and focuses it. The stats row tells you the document is 41,208
values deep at most five levels, which is usually enough to decide whether the shape is
what your code expects.
4. Shrinking a payload before you ship it
A configuration blob checked into a repository, pretty-printed with four-space indent, is 214 KB. Press Minify and the stats row shows the output at 137 KB — a 36% reduction, entirely from whitespace.
Worth knowing before you act on that: if the response is served with
Content-Encoding: gzip or br, most of that saving is already
being made for you, because repeated runs of spaces compress extremely well. Minifying a
gzipped API response typically buys single-digit percentages on the wire. Where minifying
genuinely pays is in things that are not compressed in transit: values stored in a
database column, embedded in a QR code, put in a cookie or a header, or held in memory as
strings. More on when minifying is worth it →
5. The number that changed by itself
Paste {"id": 9007199254740993} and format it. The output reads
9007199254740992 — one less than you typed.
Nothing here is broken; this is IEEE 754. JavaScript numbers are 64-bit floats, which represent every integer exactly only up to 253 − 1 (9,007,199,254,740,991). Above that, values are rounded to the nearest representable double. Every JSON parser built on JavaScript numbers behaves this way, including the ones in your browser and in Node.js, so a large database ID or a Twitter-style snowflake must be transported as a string. If your API emits bare 64-bit integers, this page will show you the damage before your users find it.
Frequently asked questions
Is my JSON uploaded anywhere?
No. The page has no upload endpoint and no storage. Parsing, formatting, minifying,
the statistics and the tree all run in JavaScript inside your tab, using the browser
engine’s own JSON.parse. There is no code on this page that can send
your document anywhere.
You do not have to take that on faith. Open developer tools (F12, or ⌘⌥I on a Mac), select the Network panel, paste a document and press Format. As of today the page issues no request at all once it has loaded. You can also save the page to disk with Ctrl/⌘S and use it with the network switched off entirely.
How large a document can I paste?
Documents of several megabytes are fine. Above roughly 300 KB the tool stops re-formatting on every keystroke and waits for you to press Format or Minify, because re-parsing a large document on each character typed is what makes other formatters freeze.
Two further guards apply to very large inputs: the text output pane renders only the first 250,000 characters (Copy and Download still give you the whole thing), and the tree builds nodes only as you expand them. The practical limit is your machine’s memory, not a size cap in the page.
What does “Unexpected token … in JSON at position N” mean?
It means the parser reached character number N — counting from zero, across the whole document including newlines — and found something that cannot legally appear there. The character it names is where the parser noticed the problem, which is often one or two characters after where you actually made it: a missing comma is reported at the value that follows it, not at the gap.
Paste the document here and you get the line, the column and a caret instead of a bare offset. The dedicated page works through every common cause with examples.
Why does my formatted output not match my input byte for byte?
Because the output is re-serialised from the parsed value rather than edited as text.
Four things can legitimately change: whitespace and indentation, of course; non-ASCII
characters written as \uXXXX escapes come back as the literal characters;
numbers are normalised (1.50 becomes 1.5,
1e3 becomes 1000); and duplicate keys collapse to the last
one, since an object cannot hold the same key twice.
Integer-like keys are also reordered ahead of string keys and sorted numerically. That is a rule of the JavaScript object model that every JavaScript-based JSON tool inherits. If exact byte preservation matters — for a signature, a hash or a canonical form — do not round-trip the document through any parser.
Are trailing commas or comments ever acceptable?
Not in JSON. RFC 8259 allows neither, and no conforming parser accepts them. They feel acceptable because JavaScript, Python, most linters and every code editor allow them, and because several JSON-adjacent formats deliberately add them back.
If you control the format, the options are JSON5 (comments, trailing commas, unquoted
keys, single quotes), JSONC (comments only — what VS Code’s settings files
use), YAML, or TOML. If you do not control the format, strip the extras before parsing.
Beware of the obvious shortcut: a regular expression that deletes //
comments will happily corrupt any string containing a URL.
Can it handle JSON Lines / NDJSON?
Not as a single document, and neither can any JSON parser — that is the point of the format. NDJSON is one complete JSON value per line, and the file as a whole is not valid JSON. Pasting it here produces “A second JSON value starts here” at the beginning of line two, which is the correct diagnosis.
Split on newlines and parse each line separately, or wrap the lines in
[…] with commas between them to inspect the whole file
here as one array.
Why do my big integers lose precision?
Because JSON numbers become IEEE 754 doubles. Integers above 253 − 1 = 9,007,199,254,740,991 cannot all be represented, so they round to the nearest value that can be. This is not specific to this page: it happens in your browser, in Node.js, and in any language that maps JSON numbers to a 64-bit float.
The standard remedy is to transport large identifiers as strings. Several languages offer big-decimal parsing modes that keep the digits intact, but the moment the value crosses a JavaScript boundary the precision is gone again.
Are duplicate keys allowed?
RFC 8259 says names within an object should be unique, but does not forbid repeats, and it leaves the behaviour of parsers that receive them undefined. In practice JavaScript keeps the last occurrence, and so does this page. Other parsers keep the first, or raise an error, or return a multi-map.
That disagreement has been used as an attack: a payload with a key repeated twice can be validated by one service and acted on by another with a different value. If you are writing a producer, never emit duplicates. If you are writing a consumer of untrusted JSON, reject them explicitly.
Does it work offline?
Yes. Once the page has loaded, nothing further is fetched. Save it with Ctrl/⌘S and it keeps working from the local file with the network disconnected — there are no CDN scripts or web fonts to go missing, and every link between the pages is relative.
Does it validate against a JSON Schema?
No. This checks syntax — whether the text is well-formed JSON —
not whether the resulting document matches a schema, has the fields your API requires or
uses the right types. Those are separate questions answered by a validator such as Ajv,
jsonschema or python-jsonschema. Syntax validity is the
prerequisite: a document that fails here cannot be schema-checked at all.
What are the statistics counting?
Input and Output are UTF-8 byte counts, not character
counts — an emoji is four bytes, an accented Latin letter two. Keys
is every key in every object added together, so a hundred objects with three keys each
counts as three hundred. Max depth counts nested containers: a bare
number is 0, [] is 1, {"a":[1]} is 2.
Values is every node in the document, containers included.
Is the tree keyboard-accessible?
Yes. Tab into the tree and use ↑ and ↓ to move between visible rows, → to open a node or step into it, ← to close it or jump to its parent, Enter or Space to toggle, * to open every sibling of the focused node, and Home / End for the first and last visible rows. It is a standard ARIA tree, so screen readers announce the expanded state and the level.
Use cases and limitations
What people use this for
- Reading an API response. A minified response body pasted from the network tab becomes readable in one keystroke, and the tree lets you collapse the parts you do not care about.
- Finding the syntax error in a config file.
package.json,tsconfig.json, a Terraform variables file, a Kubernetes manifest — when a tool says only “invalid JSON”, this says which character. - Checking a payload before sending it. Paste a request body you are about to POST and confirm it parses and has the shape you meant.
- Understanding an unfamiliar document. The tree plus the depth and key counts tell you the shape of a response faster than scrolling.
- Shrinking a value. Minifying before writing to a column, a cookie, a QR code or an environment variable.
- Comparing two versions. Format both with the same indent and paste them into any diff tool; identical formatting removes the whitespace noise.
What it deliberately does not do
- Repair broken JSON. It tells you precisely what is wrong and where, but it will not guess where a missing brace belongs. Automatic repair is how a subtle corruption gets committed with confidence.
- Accept JSON5, JSONC, HJSON or YAML. Comments, trailing commas, unquoted keys and single quotes are reported as the errors they are. A lenient parser would let you ship a file that your production runtime rejects.
- Validate against a schema. Syntax only.
- Preserve byte-for-byte formatting. Output is re-serialised from the parsed value; see the FAQ above.
- Sort or transform. No key sorting, no JSONPath queries, no
filtering language. For transformations,
jqis the right tool and it runs on your machine.
Honest limitations
Numbers pass through JavaScript’s IEEE 754 doubles, so integers beyond
253 and decimals such as 0.1 are subject to the usual floating-point
rounding. Very large documents are limited by tab memory: a browser tab that runs out will
be terminated by the browser, and a document larger than a few hundred megabytes is a job
for a streaming parser such as jq, ijson or Jackson’s
streaming API, not for any browser page. Lone surrogates in \u escapes are
accepted, matching JSON.parse, even though they cannot be encoded as valid
UTF-8.
More JSON tools on this site
- JSON validatorCheck syntax and get the exact line and column
- JSON minifierStrip whitespace and see the byte saving
- “Unexpected token” explainedEvery cause of the most common JSON error
- Escape & unescape stringsTurn text into a JSON string literal and back
- About this toolWhy it's built this way, and what it deliberately doesn't do