JSON Formatter & Validator

Format, minify and check JSON as you type. Errors get pointed at, not described

Try one:

Structure, and the path to every value
Nothing to show yet.

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 }              ->  1

The 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 thereHow it got there
U+FEFFA byte-order mark. Notepad, PowerShell redirection and anything Windows wrote as "UTF-8"
U+00A0A non-breaking space, from copying JSON out of a rendered web page
U+201CA 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 feed

That 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 it

Those 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.