The guide
JSON Fits on One Page. The Trouble Starts Where Software Reads It Generously
The grammar is tiny and strict, yet real files still break on commas, comments and quotes, and big numbers change silently on the way in.
- 4 whitespace characters: space, tab, line feed, carriage return
- 9007199254740991 largest integer every double-precision reader agrees on
- 1E400 an RFC 8259 example number that overflows a double
- ~0 and ~1 how a JSON Pointer writes a tilde and a slash inside a name
Read the full guide Fold the guide
JSON is one of the few formats whose whole grammar fits on a single page. A text is one value with optional whitespace around it. A value is an object, an array, a string, a number, or one of the words true, false and null. Six punctuation marks hold the structure together. That is nearly all of it, and the smallness is the point. The argument here is that this strictness is a feature worth defending, and that most of the pain people blame on JSON comes from software that reads it more generously, or more lossily, than the grammar says.
A grammar you can hold in your head
The current definition is RFC 8259, published by the IETF in December 2017, and it agrees with Ecma’s standard, ECMA-404, whose second edition appeared the same month. Douglas Crockford first described the format as an informational RFC, number 4627, in July 2006. Across those documents the core never grew. The json.org page still opens by saying JSON is built on two structures: a collection of name and value pairs, and an ordered list of values.
The details are short enough to list. Whitespace means exactly 4 characters: space, tab, line feed and carriage return. The literal names must be lowercase. A number may start with a minus sign but never a plus, may not have leading zeros, and needs at least one digit after a decimal point and in an exponent. A string sits between double quotes, must escape the quotation mark, the backslash and every control character below U+0020, and offers a fixed menu of escapes, including the four-hex-digit form \u followed by a code point.
Infinity and NaN are not on that list. RFC 8259 says so directly.
Where real files break
Most broken JSON is not exotic. It is a trailing comma left after the last item when someone deleted a line, a key written in single quotes because the author was thinking in another language, or a comment explaining a setting. MDN’s page on JSON.parse lists the first two as the classic causes of a SyntaxError, with examples. The third is a deliberate absence. Wikipedia records that Crockford took comments out early, because people had begun using them to carry instructions for the parser, which would have broken interoperability.
The usual complaint is that the parser’s message is unhelpful. Some engines report a character offset, some a line, some only the token they choke on. The right place to report an error is well defined, though.
Read the text from the left and stop at the first character that could not possibly begin a valid continuation. Everything before it could still become a valid JSON text with the right ending; that character rules every ending out. In “[1,]” that is the closing bracket at offset 3, not the comma before it, because “[1,” is still a perfectly good beginning. A parser that reports that exact position, as a line and a column, and says which tokens would have been accepted there, turns a puzzle into a ten-second fix.
Leniency is tempting, and the standard permits it: RFC 8259 says a parser must accept every conforming text and may accept extensions. Python’s json module is a good example of that freedom. By default it reads and writes NaN, Infinity and negative Infinity, and its own documentation notes that the results are not valid JSON. The danger of generosity is that a file which loads in one program fails in the next, and nobody finds out until it is in production. A lenient mode is useful when you know you are reading a configuration file with comments. It should say loudly that what it accepted was not JSON, and the file it writes back should be.
Even the edges are documented. Nicolas Seriot’s JSONTestSuite, released under the MIT licence, sorts its small test files into three groups: content a parser must accept, content it must reject, and content where the standard leaves parsers free to do either. That third group is where most disagreements between real libraries live, and the line between the groups is exactly the line a strict parser has to walk.
The number that changes on the way in
Here is a failure that produces no error at all. The JSON grammar puts no limit on the size or precision of a number. RFC 8259 notes that implementations may set limits, and that because IEEE 754 double precision is so widely available, numbers within its range and precision interoperate well. Integers between minus and plus 9007199254740991, which is 2 to the power 53 minus 1, are the range on which every such reader agrees exactly. MDN gives the same value for Number.MAX_SAFE_INTEGER, the largest integer a JavaScript number holds without loss.
Outside that range the value moves. Take the line “id”: 12345678901234567890 and hand it to JSON.parse. The nearest double is 12345678901234567168, and when JavaScript writes it back it prints 12345678901234567000. No exception is thrown and no warning appears. The RFC itself names 1E400 and 3.141592653589793238462643383279 as numbers that hint at trouble: the first overflows a double to Infinity, which JSON.stringify writes as null; the second loses its tail and comes back as 3.141592653589793.
The ecosystem has noticed. MDN points out that a reviver function passed to JSON.parse runs only after each number has already been converted, so precision is gone by the time your code sees it, and documents a context argument that hands the reviver the original source text instead. JSON.rawJSON goes the other way, letting a program write a number out exactly as given. The Go standard library stores JSON numbers as float64 by default when it decodes into an interface value, and offers UseNumber to keep them as text. The I-JSON profile, RFC 7493, recommends putting numbers that need more than double precision, such as 64-bit identifiers, into strings.
A formatter has a simple duty here. It should never be the step that rounds. If the input said 12345678901234567890, the pretty-printed output should say 12345678901234567890 too, and the tool should tell you which numbers a JavaScript reader would change, by how much, before you ship the file to one.
Duplicate keys and the last one wins
The second silent problem is a key that appears twice in one object. RFC 8259 says names should be unique, then admits that when they are not, the behaviour of receiving software is unpredictable: many implementations keep only the last pair, some report an error, and some keep every pair. JavaScript’s own specification settles its side of the question in a note on JSON.parse, where the earlier value for a repeated name is overwritten by the later one. Python’s json module documents the same choice and shows “x” given three times coming back as 3.
Not everyone agrees. The documentation of Go’s encoding/json package says its original version permits duplicate names, while its newer version treats them as an error unless an option allows them. I-JSON forbids duplicates outright, comparing names after escapes are decoded, so “a” and “a” count as the same name.
That last detail matters. A duplicate written once with an escape and once without is invisible to the eye and obvious to the parser. A useful checker reports both positions, the first and the second, and tells you which one a JavaScript reader will keep.
Naming one value: JSON Pointer
Once you can see the problems you need a way to talk about where they are. RFC 6901 defines JSON Pointer for exactly that. A pointer is a sequence of steps, each starting with a slash. A step is an object member name or an array index counted from zero, so /owner/user means the member “user” inside the member “owner”. Because the slash and the tilde have jobs to do, a tilde inside a name is written ~0 and a slash is written ~1, and the order of decoding is fixed so that ~01 means a literal ~1 and not a slash. Those two escapes, ~0 and ~1, are the whole of the escaping rule.
The edge cases are spelled out. An array index may not have a leading zero. The single character ”-” names the element after the last one, which never exists, so evaluating it is always an error unless an application says otherwise. A name that appears twice in the same object makes evaluation fail, which is one more reason to fix duplicates. The RFC also defines a form for URI fragments: the pointer is encoded as UTF-8 and every character outside the fragment rule of RFC 3986 is percent-encoded, so a step named “c%d” becomes #/c%25d.
Here is a worked example you can reproduce on this page. Load the valid sample. The checker reports it as valid JSON, with one duplicate key and two numbers that change. The duplicate is “count”, at line 14, column 3, first seen at line 13, column 3; JavaScript keeps the value 4. Select the member “a/b” and its pointer reads /a~1b. Select “id”: its position is line 2, column 9, its text is 12345678901234567890, and JSON.parse would give 12345678901234567000. Then load the broken sample. The error sits at line 4, column 28, on the closing bracket after “beta”, and the message names the trailing comma.
Formatting is a reading aid, not a change
Pretty printing changes nothing a parser cares about. JSON allows whitespace between any two tokens, and json.org says as much, so indenting by two spaces, four spaces or a tab, or removing every optional space for the smallest file, all describe the same value. JavaScript’s own JSON.stringify accepts an indent of up to 10 spaces and silently clamps anything larger.
Two options deserve a note. Sorting keys makes two versions of a document easy to compare, but the order has to be defined; sorting by Unicode code point is unambiguous, and it differs from the plain order of UTF-16 code units only when a character beyond U+FFFF meets one between U+E000 and U+FFFF. Escaping non-ASCII characters as \u sequences produces a file that survives tools which mangle UTF-8, at the cost of readability. Neither option changes the value, and a careful formatter can prove it by reading its own output back and comparing.
What a validator cannot tell you is equally worth stating. It checks the grammar, not the meaning. It cannot know that a field should have been a string, that an identifier is wrong, or that the receiving program rejects a perfectly valid document for reasons of its own; that is the work of a schema and of the program itself. It cannot tell you which of two duplicate values the author intended. And its verdict on numbers is a verdict about double precision, the common case, not about every reader on earth. Within those limits, strictness is a kindness: a file that passes a strict reader will be read the same way almost everywhere.
Sources
Every source consulted for this guide. Links checked October 18, 2026.
- The JavaScript Object Notation (JSON) Data Interchange Format (RFC 8259) IETF (T. Bray, editor), December 2017
- The application/json Media Type for JavaScript Object Notation (JSON) (RFC 4627) IETF (D. Crockford), July 2006
- ECMA-404 The JSON data interchange syntax Ecma International, 2nd edition, December 2017
- JavaScript Object Notation (JSON) Pointer (RFC 6901) IETF (P. Bryan, K. Zyp and M. Nottingham), April 2013
- The I-JSON Message Format (RFC 7493) IETF (T. Bray, editor), March 2015
- Uniform Resource Identifier (URI): Generic Syntax (RFC 3986) IETF (T. Berners-Lee, R. Fielding and L. Masinter), January 2005
- 25 Structured Data ECMAScript 2027 Language Specification (draft), Ecma International TC39; section 25.5.1 JSON.parse
- JSON.parse() MDN Web Docs
- JSON.stringify() MDN Web Docs
- JSON.rawJSON() MDN Web Docs
- Number.MAX_SAFE_INTEGER MDN Web Docs
- Introducing JSON json.org
- JSON - Wikipedia Wikipedia (CC BY-SA)
- json - JSON encoder and decoder Python documentation, Python Software Foundation
- json package - encoding/json Go Packages (the Go project)
- JSONTestSuite: A comprehensive test suite for RFC 8259 compliant JSON parsers Nicolas Seriot, GitHub (MIT licence)