JSON guide

How to Format, Minify and Validate JSON

A working reference for JSON's grammar, how to read a parser's error offset, where numbers and strings quietly lose information, and why valid JSON still gets rejected.

Formatting JSON is the easy half. The useful half is knowing what the grammar actually permits, how to turn a parser's byte offset into the character you mistyped, and which parts of a document do not survive a parse-and-reserialise round trip. This guide covers all three, with the reference tables you would otherwise go looking for in RFC 8259.

JSON's grammar is much smaller than JavaScript's

JSON was carved out of JavaScript's object-literal syntax around 2001 and later standardised twice, as ECMA-404 and as RFC 8259. The subset is deliberately tiny. There are six value types — object, array, string, number, `true`/`false`, and `null` — one string quoting style, and nothing else. No comments. No trailing commas. No unquoted keys. No `undefined`, `NaN`, `Infinity`, dates, or regular expressions.

Almost every document reported as "invalid JSON" is one that a JavaScript engine would accept without complaint. That is the single most useful thing to internalise: if you copied a structure out of a browser console, an editor's config file, or a Python `repr`, it is a JavaScript or Python object literal, not necessarily a JSON document. Round-tripping it through a strict parser is a real check, not a cosmetic one.

Every construct in the right-hand column is legal JavaScript and illegal JSON. This table resolves the large majority of parse failures on its own.

ConstructValid JSONRejected by a strict parser
Object keys{"name": "Ada"}{name: "Ada"}
String quoting{"city": "Oslo"}{"city": 'Oslo'}
Trailing separator[1, 2, 3][1, 2, 3,]
Commentsno syntax exists{"a": 1} // note
Leading zeros{"n": 7}{"n": 007}
Bare decimal point{"n": 0.5}{"n": .5} or {"n": 1.}
Explicit plus sign{"n": 1}{"n": +1}
Non-finite numbers{"v": null}{"v": NaN} or {"v": Infinity}
Missing valueomit the key entirely{"v": undefined}
Byte-order markno BOM before the first tokena U+FEFF preceding `{`

What formatting changes, and what it leaves alone

Pretty-printing inserts newlines and indentation between tokens and adds a space after each name separator. Minifying removes every byte of insignificant whitespace. Both operations are defined purely on the whitespace between tokens, which is why a formatted and a minified document parse to exactly the same structure — with the single exception of numbers, covered below.

It is worth being precise about what a formatter does not do. It does not sort keys, so key order is preserved as written; the specification does not require consumers to respect that order, so never make correctness depend on it. It does not change types: a value written as `"42"` stays a string. It does not check your document against a schema, so a perfectly formatted document can still be missing every field the receiving API requires. And it does not repair anything — a formatter either parses the input or refuses it.

Read the error offset, not the error message

Parser diagnostics are structurally misleading, and not because the parsers are bad. A parser reports the position where it encountered something that could not appear there. It cannot report the position of the thing you actually got wrong, because at the moment you typed the extra comma the document was still consistent with several valid continuations.

The practical rule is therefore: take the reported offset, then inspect the characters immediately before it. In the example below the parser names offset 35, the closing bracket, but the defect is the comma at offset 34.

Two failure shapes account for most of the remainder. An unterminated string lets the parser swallow the rest of the document, so the error surfaces hundreds of lines below the missing quote — in a formatted view this shows up as one absurdly long highlighted string. A missing closing brace produces an error at the very end of the input, or at the next token that cannot continue an object. For both, format the document down to the last block that renders sensibly and start looking at whatever follows it.

Locating a trailing comma from a byte offset
Input (one line, offsets counted from 0):

{"id":42,"tags":["beta","internal",],"owner":{"name":"Ada"}}
 0123456789...                     ^34 ^35

A strict parser reports an error at offset 35 — the ']' character,
because a ']' cannot begin a value and a value was required there.

The mistake is at offset 34: the comma after "internal".

Wording varies by runtime; the offset does not:
  V8            Unexpected token ] in JSON at position 35
  Python        Expecting value: line 1 column 36 (char 35)
  Go            invalid character ']' looking for beginning of value

Python and many other parsers report a 1-based column, so column 36
and offset 35 are the same character. Check which convention your
error message uses before counting.

Numbers are the one place JSON silently loses data

JSON's number grammar is decimal text with optional fraction and exponent, and it places no limit on magnitude or precision. The specification is explicit that interoperable implementations should expect no better than IEEE 754 double precision, and in practice nearly every parser produces a double. That mismatch is where data goes missing without any error being raised.

The concrete threat is the 64-bit identifier. Snowflake IDs, Twitter/X status IDs, Discord snowflakes, and most `bigint` primary keys exceed 2^53 - 1 (9,007,199,254,740,991). Parse one as a double and it is rounded to the nearest representable value, then re-serialised as that wrong number. Nothing in the pipeline reports a problem; the record simply refers to a different row. The fix is upstream: serialise identifiers as strings. If you only need to read the document, read the raw text rather than a parsed view.

A round trip through a JavaScript engine
Input
{"id": 9007199254740993, "ratio": 0.1, "big": 1e400, "zero": -0, "amount": 4.50}

After JSON.parse() then JSON.stringify()
{"id":9007199254740992,"ratio":0.1,"big":null,"zero":0,"amount":4.5}

  id      rounded down: 2^53 + 1 is not representable as a double
  ratio   survives: the shortest round-trip decimal is printed back
  big     1e400 overflows to Infinity, which stringify emits as null
  zero    -0 is a valid double but stringify prints it as 0
  amount  the trailing zero is not part of the value, only the text

Only one of these five raises anything you could catch.

Strings, escapes and encoding

A JSON string is a sequence of Unicode code points in double quotes. Two characters must always be escaped — the double quote and the backslash — and every control character below U+0020 must be escaped, either with one of the short forms or with a `\u` sequence. Everything else may appear literally, including the forward slash, which has a legal `\/` escape that is never required.

Characters outside the Basic Multilingual Plane, such as emoji, can be written literally in UTF-8 or as a surrogate pair of `\u` escapes. Both are valid and parse to the same string. RFC 8259 requires UTF-8 for JSON exchanged between systems, and a byte-order mark is not part of the grammar — `JSON.parse` on a string with a leading U+FEFF throws, which is a common and confusing failure when a file was saved by a Windows editor.

CharacterEscapeRequired?
Double quote"Always
Backslash\\Always
Forward slash\/Never — legal but optional
Backspace U+0008\bAlways (or `\u0008`)
Form feed U+000C\fAlways (or `\u000c`)
Line feed U+000A\nAlways — a raw newline is a parse error
Carriage return U+000D\rAlways
Tab U+0009\tAlways
Other U+0000–U+001F\u00XXAlways
Emoji, CJK, any BMP+ characterliteral UTF-8 or a `\u` pairNever

Duplicate keys and other undefined behaviour

RFC 8259 says the names within an object SHOULD be unique — a recommendation, not a requirement — and then says the behaviour of an implementation receiving duplicates is unpredictable. That is an unusually honest piece of specification writing, and it means `{"a": 1, "a": 2}` has no defined result.

In practice most implementations keep the last occurrence: JavaScript, Python's `json`, and Go's `encoding/json` all do. But some parsers keep the first, some collect all values into a list, and some reject the document outright. Security tooling has repeatedly exploited exactly this gap, where a proxy and an origin server disagree about which value they saw. If you are generating JSON programmatically, never rely on later keys overwriting earlier ones — deduplicate before serialising.

Valid is not the same as correct

A formatter proves that a document parses. It cannot tell you that the document is the one the receiving system wants. Those are different questions and they need different tools. If a request is rejected with a 400 after your JSON formats cleanly, the problem is almost always shape, not syntax: a required property is absent, a number arrived as a string, an enum value is misspelled, or a nested object was sent where an array was expected.

JSON Schema is the standard answer for shape. It expresses required properties, types, enumerations, string patterns, numeric ranges, and conditional requirements, and validators exist for every major language. Where an API publishes an OpenAPI description, the schemas in it are JSON Schema and can be run against your payload directly. That gives you a real pass/fail against the contract rather than against the grammar.

  • Syntax question: does it parse? Use a formatter or validator.
  • Shape question: does it match the contract? Use JSON Schema or the OpenAPI document.
  • Extraction question: what is at this path? Use JSONPath or `jq`.
  • Comparison question: what changed between two payloads? Format both, then diff.

What to remember

  • Treat anything with comments, trailing commas, single quotes or unquoted keys as a JavaScript object literal rather than JSON, and convert it deliberately instead of guessing why the parser complained.
  • When a parse fails, read the reported offset and inspect the characters immediately before it, because a parser can only report where a value was expected, never where the extra separator was typed.
  • Serialise 64-bit identifiers and monetary amounts as strings, since JSON numbers go through IEEE 754 doubles and lose both large integers and trailing decimal zeros with no error.
  • Deduplicate object keys before serialising, because the specification leaves duplicate handling undefined and different parsers in the same request path can disagree about which value won.
  • Use a formatter for syntax and JSON Schema for shape; a document that formats perfectly can still be rejected by the API it was written for.

Continue with related checks and tools