JSON Formatter & Validator
Format, minify and check JSON as you type. Errors get pointed at, not described
Worth knowing
Structure, and the path to every value
Object, array, string, number, boolean, null. That is the entire type system, and most arguments about what counts as JSON come down to somebody remembering a seventh that was never there.
value object | array | string | number | true | false | null
object { } or { "key": value, "key": value }
array [ ] or [ value, value ]
string "..." double quotes only, always
number -?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?There is no date, no comment, no integer distinct from a float, no trailing comma, and no way to write a number in hex. A top-level value may be any of the six, which was not true of the original specification and is why some older parsers still reject a bare "hello" or 42 as a whole document.
The number that came back different
This is the one that costs real money. JSON puts no limit on how long a number may be, and nearly every parser reads it into a 64-bit float, which holds about fifteen and a half significant decimal digits. Anything longer is rounded on the way in, silently, and there is nothing downstream that can recover it.
{ "id": 9007199254740993 } -> 9007199254740992
{ "id": 1234567890123456789 } -> 1234567890123456800
{ "price": 1e400 } -> Infinity, then null on the way out
{ "version": 1.0 } -> 1The safe range for an integer is -(2^53)+1 to(2^53)-1, which is 9007199254740991: sixteen digits. A Twitter, Discord or database snowflake ID is 64 bits and eighteen or nineteen digits, so every API that issues one also returns it as a string, and why the ones that forgot to have a long tail of bug reports about IDs ending in 00.
This tool never parses a number. It keeps the characters as they arrived and writes the same characters back, so formatting a document here cannot round anything, and the summary tells you which values would have been damaged had it done so. It also tells you where 1.0 would have come back as1, which is harmless until something downstream is checking whether a field is an integer.
Two keys with the same name, and five parsers that agree by accident
The specification says names within an object should be unique, and says nothing about what to do when they are not. In practice everything keeps the last one and mentions nothing:
{ "role": "viewer", "role": "admin" }
JavaScript JSON.parse(...).role -> "admin"
Python json.loads(...) -> "admin"
Go map[string]any -> "admin"
Java Jackson, default -> "admin"
Ruby JSON.parse -> "admin"Which is fine until the document was assembled by concatenating two things, or a templating loop emitted the same field twice, or somebody is exploiting the fact that the parser in front of your API and the parser behind it pick different ones. Duplicates get listed above as a warning instead of resolved without comment, because the only reason you will ever want to know is that you did not expect them.
What is not JSON, whatever the file is called
{
// a comment not JSON
/* nor this one */ not JSON
'single': 'quotes', not JSON
unquoted: "keys", not JSON
"trailing": "comma", not JSON
"hex": 0xff, not JSON
"leading": .5, not JSON
"plus": +1, not JSON
"missing": undefined, not JSON
"special": NaN, not JSON
}Comments were in the first draft and were taken out on purpose, because people had started using them to carry parsing directives. The result is that every configuration format that wants comments has had to invent a dialect: JSONC for tsconfig.json and everything VS Code writes, JSON5 for the fuller set, and HJSON, and YAML, and TOML.
Switch on the relaxed dialect above and all of it is read. What comes out is always strict JSON, with the differences listed: NaN andInfinity become null, because that is what every serialiser does with them and there is no other option available.
Where the error actually is
"Unexpected token" with no position is the least useful sentence in programming. Four things cause most of these, and three of them are invisible on screen:
| What is there | How it got there |
|---|---|
| U+FEFF | A byte-order mark. Notepad, PowerShell redirection and anything Windows wrote as "UTF-8" |
| U+00A0 | A non-breaking space, from copying JSON out of a rendered web page |
| U+201C | A curly quote, from a document editor that helpfully replaced the straight one |
| ,] | A trailing comma left behind when the last element was deleted |
Each of those is named outright above, with a caret under the character responsible and the line and column beside it. The caret is worth more than the message on a minified file, where every error is on line 1.
Strings are UTF-16 with a very short escape list
\" quotation mark \n line feed
\\ reverse solidus \r carriage return
\/ solidus (optional) \t tab
\b backspace \uXXXX one UTF-16 code unit
\f form feedThat is the whole list. There is no \x41, no \0, no\', and no line continuation: a string cannot contain a literal line break, so multi-line text in JSON arrives as one long line full of\n. A raw tab inside a string is invalid too, though a great many parsers accept one anyway.
\uXXXX escapes one UTF-16 code unit, not one character. Characters outside the basic plane take two of them, and if only one arrives you have half a character that no encoder can turn into UTF-8:
"🌞" one character, two code units, four UTF-8 bytes
"\ud83c\udf1e" the same character, written out
"\ud83c" half a character, and no valid UTF-8 encodes itThose halves are reported as warnings above, because they survive being parsed and then fail somewhere much later, usually in whatever writes to the database. Switch on Escape non-ASCII to write every character above U+007E as an escape. Use that when the file has to survive a system that mangles anything that is not ASCII.
Two characters are always escaped here even though they are legal:U+2028 and U+2029. They are line terminators in JavaScript, so a JSON value containing one used to break as soon as it was pasted into a script or served as JSONP. ES2019 made JavaScript accept them inside string literals, which fixed the language but not the twenty years of tooling written before it.
One document per line
NDJSON, also sold as JSON Lines, is a whole document on each line with no commas between them and no brackets around the lot:
{"ts":"2026-08-14T09:00:00Z","level":"info","msg":"started"}
{"ts":"2026-08-14T09:00:04Z","level":"warn","msg":"retrying"}
{"ts":"2026-08-14T09:00:09Z","level":"error","msg":"gave up"}It exists so that a writer can append one line to a log and a reader can handle one line without holding the whole file, which an array cannot do because the closing bracket is at the end. Every data warehouse export and most structured logging is this. Switch the input above to read it, and the output to write it: an array in, one item a line out.
A broken line does not invalidate the others, so the error names the line and everything before it still parsed.
Sorting the keys changes nothing, and that is the point
Objects are unordered by definition, and every parser worth using preserves the order anyway, which means two exports of the same data can differ by nothing but key order and produce a diff a thousand lines long. Sorting both before comparing collapses it to the changes that are real.
The sort here is natural, so item2 comes beforeitem10 instead of after it, and ties break by code point so the result does not depend on the machine's locale. It applies at every level.
Minifying is worth less than it looks
Stripping the whitespace out of a formatted document typically takes a fifth off, sometimes a third. Then the server gzips it, and indentation is the most compressible thing in a file: the same handful of byte sequences over and over. Most of what minifying saved was already free.
Where it does pay is anywhere the bytes are counted before compression, or where there is no compression at all: a message queue, a database column, a cookie, a URL parameter, a log line. Everywhere else, the readable version costs almost nothing and can be read in a terminal.