codr.tools
Tools

JSON formatter / validator

Format, minify and validate JSON in your browser, with syntax errors explained by line and column, repeated keys listed, and number literals kept exactly as written.

Direction
Direction
In282 bytes
Out469 bytes
Swap
Ratio×1.66
StatusValid

1 number here cannot be read back exactly by a reader built on IEEE 754 doubles, which is what JSON.parse and most other readers are. The digits are kept as written in the output.

What is in this document

Objects
5
Arrays
2
Keys
17
Strings
7
Numbers
6
Booleans
1
Nulls
1
Depth
4
Longest array
2

Numbers a reader will change

These literals do not fit an IEEE 754 double, which is what JSON.parse and most other readers use. The digits are kept as written above, but this is what comes back after a round trip.

WhereLineWritten, then read back
$.order.id1:169007199254740993 → 9007199254740992
Keys

Sorts every object by key, by code point, the way jq -S and Python's sort_keys=True do. Array order is never touched.

Format, minify and validate JSON in your browser, with syntax errors explained by line and column, repeated keys listed, and number literals kept exactly as written.

How to use this tool#

  1. Pick a direction. Format indents the document; Minify puts it on one line. Both run as you type, so there is no button to press.
  2. Paste your JSON over the example. The example is there so you can see what the output and the report look like before you commit anything.
  3. Choose the indentation. 2 spaces, 4 spaces or a tab. 2 is whatjq and most editors use; Python's json.tool uses 4.
  4. Read the message under the output. A syntax error names the line and the column, says in plain words what is wrong, and shows the offending line with a caret under the spot.
  5. Check the report below the boxes. It counts what is in the document and lists the two things that quietly bite: keys that appear twice in one object, and numbers that no reader built on doubles can give back.
  6. Turn on Sort keys when you are about to diff two documents, and press Swap to put the output back in the input if you want to run it the other way.

The tool doubles as a JSON validator: if the document parses, the formatted text appears and the status lamp turns green; if it does not, you get the position of the first problem. Everything happens in your browser, so it is safe to paste a config or a payload that has secrets in it.

Formatting only moves whitespace#

JSON allows whitespace around any of its six structural characters, {,}, [, ], : and ,, and nowhere else. Indenting a document and putting it back on one line are therefore the same operation run twice: the tokens do not move, only the gaps between them.

javascript
// The same document, three ways. All three carry the same data.

{"name":"api","ports":[80,443],"tls":true}

{
  "name": "api",
  "ports": [80, 443],
  "tls": true
}

{
        "name"  :  "api" ,
   "ports" : [ 80 , 443 ] ,
     "tls" : true
}

That is the whole of pretty-printing. What separates one formatter from another is what it does to the tokens it is not supposed to touch, and this one touches none of them:

  • Numbers are copied digit for digit. An id of9007199254740993 comes out as 9007199254740993, not as the rounded value a formatter built on JSON.parse would give you.
  • Strings are copied as written. \u00e9 stays\u00e9 and é stays é; neither is rewritten as the other.
  • Key order is kept, unless you ask for sorting.
  • Repeated keys are kept, both of them, and listed in the report.
  • A byte order mark is dropped, with a note, because it is legal at the head of a UTF-8 file and not legal inside a JSON text.

What the error message tells you#

A parser stops at the first thing it cannot read. The useful part is not the position on its own, it is knowing which of the dozen usual mistakes you made, so the message here names it.

json
{
  "name": "api",
  "port": 8080,
  "tags": ["a", "b",]
}
text
A comma before the closing bracket, with no value after it.
JSON has no trailing commas; JSON5 and JSONC do (line 4, column 21)

4 |   "tags": ["a", "b",]
  |                     ^

These are the ones that come up, in rough order of how often:

  • A trailing comma after the last member or item. Valid in JavaScript, valid in JSON5 and JSONC, never valid in JSON.
  • A comment. //, /* */ and # are all syntax errors. JSON has no comments at all, which is why config formats that want them use a different reader.
  • Single quotes, or unquoted keys. {'a': 1} and{a: 1} are JavaScript object literals, not JSON. Keys are always double-quoted strings.
  • Another language's literals. True, False andNone are Python; undefined is JavaScript; NaN andInfinity have no JSON spelling at all.
  • A backslash that is not an escape, nearly always a Windows path."C:\Users" fails at \U; the backslash has to be doubled.
  • A raw line break or tab inside a string. Everything below U+0020 has to be escaped, so a multi-line value needs \n.
  • Curly quotes and non-breaking spaces, which arrive when the document has been through a word processor, a chat app or a PDF. The tool names the character rather than printing a look-alike.
  • Several documents in a row. A JSON text holds exactly one value, so a file of one object per line is NDJSON and needs reading a line at a time.
  • A leading zero. 0123 is not a number in JSON, because there is no octal notation to fall back on.

The grammar, in one screen#

JSON is defined twice, in wording that the two documents promise to keep identical:RFC 8259 at the IETF andECMA-404 at Ecma International. RFC 8259 replaced RFC 7159, which replaced RFC 4627, the original.

text
value    =  object / array / string / number / "true" / "false" / "null"

object   =  "{" [ member *( "," member ) ] "}"
member   =  string ":" value
array    =  "[" [ value *( "," value ) ] "]"

; whitespace, allowed around any of the six structural characters
ws       =  *( %x20 /  ; space
               %x09 /  ; horizontal tab
               %x0A /  ; line feed
               %x0D )  ; carriage return

The parts that catch people out:

  • Only four whitespace characters are allowed between tokens: space, tab, line feed and carriage return. A non-breaking space or a zero-width space is a syntax error, not whitespace.
  • A top-level scalar is fine. 42 and "hello" are complete JSON texts. RFC 4627 required an object or an array; RFC 7159 dropped that in 2014, and RFC 8259 kept the change. Old validators may still disagree.
  • Numbers are decimal only. No 0x, no leading zeros, no trailing dot, no + in front, and RFC 8259 says plainly that "numeric values that cannot be represented in the grammar below (such as Infinity and NaN) are not permitted".
  • Strings must escape three things: the double quote, the backslash and every control character from U+0000 to U+001F. Everything else may appear as itself.
  • UTF-8 for anything that leaves the machine. Section 8.1 is a MUST for JSON exchanged between systems, and it adds that implementations "MUST NOT add a byte order mark (U+FEFF) to the beginning of a networked-transmitted JSON text", while a parser "MAY ignore the presence of a byte order mark rather than treating it as an error". This tool ignores it and says so; JSON.parse and Python's json module both reject it.
text
"\""     a double quote          "\n"     a line break
"\\"     a backslash             "\r"     a carriage return
"\/"     a forward slash         "\t"     a tab
"\b"     a backspace             "\uXXXX" any code point, four hex digits
"\f"     a form feed

# Everything else after a backslash is an error, which is why a
# Windows path is the classic one:

{"path": "C:\Users\ada"}     ->  \U is not a JSON escape
{"path": "C:\\Users\\ada"}   ->  valid, and the value is C:\Users\ada

Numbers are where JSON quietly loses data#

The grammar puts no limit on how many digits a number may have, and no reader implements that. RFC 8259 section 6 says so in as many words: good interoperability comes from expecting "no more precision or range" than an IEEE 754 binary64 double provides, and "numbers that are integers and are in the range [-(2**53)+1, (2**53)-1] are interoperable in the sense that implementations will agree exactly on their numeric values".

253 is 9,007,199,254,740,992. Above it, the doubles run out of integers, and a JavaScript reader silently picks the nearest one it has.

text
# Written in the file        # What JSON.parse gives back

9007199254740993            9007199254740992     the last digit is gone
1000000000000000000000      1e+21                same value, new spelling
3.14159265358979323846      3.141592653589793    cut to 17 digits
1e400                       Infinity             then null on the way out
1e-400                      0                    the value is gone

This tool never parses a number into a double, so the literal you see in the output is the literal you typed. It does check each one, and lists in the report every number that a reader built on doubles would hand back differently, with the value it would hand back.

shell
# node, reading and writing a document with a 64-bit id
$ node -e 'const fs=require("fs");
  console.log(JSON.stringify(JSON.parse(fs.readFileSync(0,"utf8"))))' \
  < order.json
{"id":9007199254740992}

# jq 1.8.2 hands the literal back untouched, because it never did
# arithmetic on it
$ jq -c . order.json
{"id":9007199254740993}

# until you touch it
$ jq -c '.id + 0' order.json
9007199254740992

# python integers have no size limit, so the value survives
$ python3 -m json.tool --compact order.json
{"id":9007199254740993}

Which explains a whole class of bug reports: a 64-bit database id or a snowflake travels through a JavaScript service and comes back off by one. The fixes are all on the sending side. Send the id as a string, which is why so many APIs carry both a numeric id and a string version of it; or keep the parse and the serialisation away from doubles.

JavaScript now has the second option built in. JSON.rawJSON writes a number as exact text, and a JSON.parse reviver gets the source text of each value it is handed, both of which went into ECMAScript 2026. They landed in Chrome 114, Firefox 135, Safari 18.4 and Node 21, so check what has to run your code before relying on them. Elsewhere the equivalents are Python's json.loads(parse_int=...) and Go'sjson.Number.

Two keys with the same name#

Nothing in the grammar stops an object from carrying the same key twice, and every parser resolves it differently on purpose. RFC 8259 section 4 is unusually candid about it: "The names within an object SHOULD be unique", and "when the names within an object are not unique, the behavior of software that receives such an object is unpredictable. Many implementations report the last name/value pair only. Other implementations report an error or fail to parse the object, and some implementations report all of the name/value pairs, including duplicates."

json
{
  "timeout": 30,
  "retries": 3,
  "timeout": 60
}
text
# Run through three readers, keeping the whole document:

node, JSON.parse then stringify   {"timeout":60,"retries":3}
python3 -m json.tool --compact    {"timeout":60,"retries":3}
jq 1.8.2 -c .                     {"timeout":60,"retries":3}

# The last value wins in all three, and the key keeps the position of
# its first appearance. The specification promises neither, so a fourth
# reader that takes the first value is just as conformant.

A repeated key is nearly always a copy and paste mistake, and occasionally something worse: when two services in a chain resolve duplicates differently, the document one of them validated is not the document the other one acts on. That is a real source of authorisation bugs, so the tool lists every repeat with its path and its line rather than quietly keeping one.

The output keeps both members. It is the one place where the formatted text is not something every reader agrees on, and hiding it would be worse.

Sorting keys, and when not to#

Key order carries no meaning in JSON: two documents with the same members in a different order hold the same data. Sorting is still useful, because a diff of two unsorted documents is full of moved lines. Sort both and the diff shows only what actually changed.

The Sort keys option orders every object by key, by Unicode code point, at every level. Array order is never touched, because there it is the data. The same ordering comes out ofjq -S and Python's json.dumps(sort_keys=True), so the three agree on the same document.

Minifying: what you actually save#

Minified JSON is the same document with the gaps removed. It is worth measuring what that buys, because the answer depends entirely on whether the bytes are compressed afterwards.

text
# One document: 4000 records, each with six fields.

                    on disk     gzip -9     brotli
minified            359 KB      33.1 KB     13.7 KB
2-space indent      603 KB      34.5 KB     13.3 KB
4-space indent      779 KB      36.5 KB     14.4 KB

# Formatting costs 68% more bytes on disk and 4% more over the wire.
# Under brotli the formatted file is the smaller of the two.

Indentation is highly repetitive, which is exactly what a compressor is good at, so most of the difference disappears the moment the response is gzipped. Minify for the places compression does not reach: a value embedded in an HTML attribute, an environment variable, a single-line log record, a column in a database. For an HTTP API, turning on compression is worth far more than minifying the payload.

JSON, JSONC, JSON5 and NDJSON#

Several formats look like JSON and are not, which is why a file that a tool reads happily can still fail here.

  • JSONC is JSON with comments, and in practice trailing commas too. It is whattsconfig.json and Visual Studio Code's settings files are written in. Node and browsers cannot read it without a separate parser.
  • JSON5 goes further. Its 1.0.0 specification adds comments, trailing commas, single quotes, unquoted keys, hexadecimal numbers, leading and trailing decimal points, a leading +, and NaN and Infinity. It is a superset of JSON, so every JSON document is valid JSON5, but not the other way round.
  • NDJSON, also called JSON Lines, is one complete document per line with no wrapping array and no commas, saved as .ndjson or .jsonl. Logs and streaming APIs use it because a reader can process a line without waiting for the end of the file.
shell
# NDJSON, also called JSON Lines: one document per line, no commas,
# no wrapping array. Common in logs and in stream APIs.

{"level":"info","msg":"started","at":"2026-05-01T09:24:11Z"}
{"level":"warn","msg":"retrying","attempt":2}

# It is not a JSON document, so a strict reader stops at the second
# line. Split it first:

jq -c . logs.ndjson          # jq reads a stream of documents
jq -s . logs.ndjson          # -s wraps the stream in one array

This tool reads strict JSON only, and says which of these you have handed it. If you need the relaxed forms, strip the comments first, or use the reader that expects them.

Doing it on the command line#

jq

shell
# Format, 2 spaces, which is jq's default
jq . input.json

# 4 spaces, or tabs
jq --indent 4 . input.json
jq --tab . input.json

# Minify, and sort every object's keys
jq -c . input.json
jq -S . input.json

# Validate only: no output, exit status 0 when it parses and 5 when it
# does not (jq 1.8.2). python3 -m json.tool exits 1 on a bad document.
jq . input.json > /dev/null

jq is the one to reach for, with one caveat worth knowing: since version 1.7 it keeps the original text of a number it has not done arithmetic on, so large integers survivejq . untouched, but not jq '.id + 0'.

Python

shell
# Format. The default indent is 4 spaces, not 2.
python3 -m json.tool input.json

# 2 spaces, sorted keys, minified
python3 -m json.tool --indent 2 input.json
python3 -m json.tool --sort-keys input.json
python3 -m json.tool --compact input.json

# Keep non-ASCII as itself rather than \u escapes
python3 -m json.tool --no-ensure-ascii input.json

Python's integers have no size limit, so a 64-bit id round-trips throughjson.tool intact. Its default indent is 4 spaces, which is why files formatted by a Python script look different from files formatted by jq.

Node

JSON.stringify(JSON.parse(text), null, 2) is the obvious one-liner and it is fine for documents whose numbers all fit in a double. Anything with a 64-bit id needsJSON.rawJSON, a reviver that reads the source text, or a parser that hands you strings.

Questions people ask#

Is my JSON uploaded anywhere?

No. The parser is JavaScript running in this page, the site is static files with no backend, and nothing is sent anywhere. Open the network panel, or disconnect from the network and keep typing: the tool carries on working. That is the quickest way to prove it to yourself before pasting a payload with credentials in it.

Does formatting change my data?

Not here. Whitespace is added or removed and nothing else: numbers keep their digits, strings keep their escapes, members keep their order, and repeated keys are both kept. Turning on Sort keys is the only option that reorders anything.

Why is my trailing comma an error when my editor accepts it?

Your editor is probably treating the file as JSONC or JSON5, which both allow it. Strict JSON does not, and neither does JSON.parse, Python's json module or jq. Since files like tsconfig.json are read by a JSONC parser, the comma is fine there and would be an error in a file handed to a plain JSON reader.

Can it repair broken JSON?

No, and on purpose. Guessing what a malformed document was meant to say is how data goes missing. The tool points at the first problem, in plain words, and leaves the edit to you.

Is this a JSON schema validator?

No. It checks the syntax: whether the document is JSON at all. It cannot tell you that a field is missing or that a value is out of range, which is whatJSON Schema and a validator such asajv or check-jsonschema are for.

What happens to a very large file?

It is parsed in your browser like anything else, so the limit is your machine rather than an upload size. A one megabyte document is comfortable. The output box shows the first 200,000 characters to keep scrolling smooth, while copy gives you the whole thing.

Why did my big ID come back wrong from another tool?

Because it was parsed into a 64-bit float somewhere along the way, and above 253 the floats have gaps. Paste the document here and look at the report: every number that changes under such a reader is listed with the value it would change into.

What does the depth number in the report mean?

How many objects and arrays you are inside at the deepest point in the document. A flat object is 1, an object holding an array of objects is 3. It is a quick sanity check when a document is nested more than you expected, and the tool stops at 1000 levels, deep enough for any real document and shallow enough to keep the browser's stack intact.

  • Convert YAML to JSON and JSON back to YAML in your browser, following YAML 1.2, with anchors and merge keys resolved, multi-document streams, and syntax errors reported by line and column.

  • Convert CSV to JSON and JSON back to CSV in your browser, following RFC 4180, with the delimiter detected, quoted fields and line breaks handled, optional type inference, and errors reported by line and column.

  • Pretty-print, minify and check XML in your browser, with well-formedness errors reported by line and column, and comments, CDATA sections, processing instructions and mixed content all kept intact.

  • Compare two texts or two JSON documents in your browser: a line diff with word and character highlighting inside changed lines, unified and side-by-side views, options to ignore whitespace, case and blank lines, and a copyable unified patch.