Skip to content
devtoolbox.cc

How to format and validate JSON without breaking your data

· 7 min read

Open the JSON Editor tool

JSON looks simple enough that most people never read the spec. Then an API returns 40 KB on a single line, a config file refuses to load because of one trailing comma, or an order id quietly changes its last digit. This article covers the JSON rules that matter in practice, the errors you get when you break them, and how to format and validate JSON without changing the data by accident.

To try the examples as you read, open the JSON formatter and validator in another tab. It runs in the browser, so pasting production data there doesn’t send it anywhere.

The whole JSON grammar in one screen

JSON is defined by RFC 8259. The grammar is tiny. A value is one of six things:

Type Example Notes
object {"id": 1, "tags": []} Keys must be strings in double quotes
array [1, "two", null] Items can have different types
string "Hello, \"world\"\n" Double quotes only; escapes start with \
number -12.5e3 No leading zeros, no +, no NaN
boolean true, false Lowercase only
null null Lowercase only

Most of what people reach for out of JavaScript habit is invalid. A trailing comma ({"a": 1,}) breaks the parse, and so do single quotes ({'a': 1}) and unquoted keys ({a: 1}). There are no comments of either kind. undefined, NaN and Infinity don’t exist, hex literals like 0x1F aren’t allowed, and neither is a leading zero, so 007 has to be a string.

Formats such as JSON5 and JSONC (the one VS Code uses for settings.json) allow comments and trailing commas. They are handy for hand-written config, but a strict parser rejects them, so check which one a file is supposed to be before you “fix” it.

Reading the error messages

Different parsers describe the same mistake very differently. Here is what Node.js 22 prints for two classic errors:

JSON.parse('{"a":1,}');
// SyntaxError: Expected double-quoted property name in JSON at position 7 (line 1 column 8)

JSON.parse("{'a':1}");
// SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)

Neither message says “trailing comma” or “single quotes”. The position is the useful part: it points at the character where the parser gave up, which is usually just after the real mistake. Older browsers and runtimes report only an offset, which is painful on a minified document where everything sits on line 1.

The JSON validator reports the line and column and highlights the line in the editor. For a minified blob, format it first; if it doesn’t parse, the error position still takes you straight to the bad character.

Formatting and minifying

Pretty-printing only changes whitespace between tokens. Any tool that does more than that is not formatting any more. The standard options:

JSON.stringify(data, null, 2); // 2-space indent
JSON.stringify(data, null, '\t'); // tabs
JSON.stringify(data); // minified
jq . response.json          # pretty-print
jq -c . response.json       # minify
python -m json.tool file.json

Two options are worth knowing about:

  • Sort keys. Sorting object keys alphabetically (recursively) makes two versions of the same document line up in a text diff. jq -S does it on the command line, and the editor has a “Sort keys” button. Arrays keep their order, because in JSON array order is data.
  • Indentation. 2 spaces is the most common style, 4 is standard in Python projects, and tabs keep files smaller. None of these change the data.

If you only need to see what changed between two versions of a document, sorting and diffing text is a workaround. A structural comparison, like the one in JSON Diff, ignores key order and formatting completely.

The trap nobody warns you about: numbers

JSON numbers have no size limit in the spec. JavaScript numbers do. Anything above 2^53 − 1 (9007199254740991) can’t be represented exactly, and JSON.parse rounds it without an error:

JSON.parse('{"id": 9007199254740993}').id;
// 9007199254740992

The last digit changed. If that value is an order id, a Twitter/X snowflake id or a database BIGINT, you now have a different id. And because formatting tools usually parse and re-serialize, a formatter built on JSON.parse corrupts the number just by pretty-printing it.

This is why APIs such as Twitter’s historically sent id_str next to id, and why many teams send large ids as strings. If you control the API, do the same. If you don’t, use a parser that keeps numbers exactly:

  • In JavaScript, JSON.parse with a reviver can’t help, because the value is already rounded when the reviver sees it. Use a lossless parser library, or the context.source argument from the newer “JSON.parse source text access” proposal where your runtime supports it.
  • In Python, the built-in json module keeps integers exact (Python ints are arbitrary precision), and json.loads(s, parse_float=Decimal) does the same for decimals.
  • jq 1.7 and later keep the precision of large number literals as long as you don’t do arithmetic on them; older versions round them.

The JSON editor on this site uses its own parser that keeps every number exactly as written. 9007199254740993 stays 9007199254740993 through format, minify and sort, and 1.0 stays 1.0 instead of becoming 1.

Other things that change silently

A few more round-trip surprises from JSON.parse followed by JSON.stringify:

Input After a parse and stringify round trip Why
{"a": 1, "a": 2} {"a":2} Duplicate keys: the last one wins
{"n": 1.0} {"n":1} Numbers are re-printed
{"x": 1e2} {"x":100} Same reason
{"d": "2026-09-30"} unchanged Dates are just strings in JSON

Duplicate keys are technically allowed by RFC 8259 (“names SHOULD be unique”), but different parsers keep different values, so treat them as a bug in whoever produced the document.

In the other direction, JSON.stringify drops undefined values, turns NaN into null and serializes a Date as an ISO string. Those are JavaScript behaviours, not JSON ones, and they only matter when your code builds the document.

Working with a large document

Once a JSON response grows past a few hundred lines, scrolling through text stops working. Three habits help:

  1. Collapse it. A tree view lets you open only the branch you care about. In the JSON editor, switch to Tree, expand one level, and click a node to copy its path (for example $.items[0].id).
  2. Search keys and values, not text. Searching for "id" in raw text also finds "paid" and "idempotency_key". A search over keys and values is more precise.
  3. Query instead of scrolling. If you need all items with a certain property, a JSONPath query such as $.items[?@.status == 'failed'].id answers it in one line.

Large files are also where browser tools freeze. The editor here formats documents over 500 KB in a background worker, so a 5 MB file still leaves the page responsive.

Validating structure, not only syntax

“Valid JSON” only means the text parses. It doesn’t mean the document has the fields your code expects. {"price": "12.50"} is perfectly valid JSON and still breaks code that expects a number.

Checking structure is the job of JSON Schema. A schema says, for example, that price is a number, id is required and no other fields are allowed. The JSON Schema validator checks a document against a schema and lists every problem with its path.

A short checklist

When a JSON document misbehaves, this order usually finds the problem fastest:

  1. Does it parse? Look at the reported line and column, not only the message.
  2. Is it really JSON, or JSONC/JSON5 with comments and trailing commas?
  3. Are there numbers above 2^53 that your tools might round?
  4. Are there duplicate keys?
  5. Does it have the structure you expect? That’s a schema question, not a syntax one.

Paste the document into the JSON formatter and validator and go through the list. Most broken JSON is broken in one of these five ways.

← All posts