Skip to main content

JSON Editor & Validator

Edit, format, minify, and validate JSON with syntax highlighting and error reporting—data stays local.

Loading...

Paste JSON to check whether it parses. Parsing is a lower bar than most people assume: RFC 8259 permits duplicate keys, JavaScript rounds integers past 2^53 without warning, and several values come back from a stringify-then-parse round trip as something other than what went in.

Duplicate keys are legal, and the loser vanishes

RFC 8259 section 4 states that the names within an object SHOULD be unique. That is a recommendation, not a requirement, so {"a":1,"a":2} is conforming JSON and a validator reporting it as valid is telling you the truth.

What happens next is left to the implementation, and in practice nearly everything keeps the last occurrence. JSON.parse('{"a":1,"a":2}') evaluates to { a: 2 }; Python's json.loads behaves the same way. The first value is not reported, not warned about, and not recoverable.

Detecting this after the fact is hard because no JavaScript-based formatter can show you the problem. A JavaScript object cannot hold the same key twice, so the duplicate is already gone before the pretty-printer runs — reformatting the document destroys the evidence. Duplicates usually arrive from string-concatenated templates, merged configuration fragments, or a code path that appends a field it already wrote. The security-relevant version is that two services need not agree on the winner: if one component authorises a request by reading the first occurrence and another acts on it by reading the last, one document means two things. Where a payload is signed or authorised, reject duplicates rather than resolving them.

Integers above 2^53 come back as a different number

RFC 8259 section 6 places no limit on the magnitude or precision of a number, but it notes that good interoperability is achieved when implementations expect no more range or precision than IEEE 754 binary64 provides. JavaScript does exactly that: every JSON number becomes a double.

Number.MAX_SAFE_INTEGER is 9007199254740991. Parse {"id": 9007199254740993} and you get 9007199254740992 back — no exception, no warning, an identifier off by one that now points at a different row. Nineteen-digit Snowflake IDs, PostgreSQL bigint primary keys, and long account numbers all sit above this line. Twitter shipped a separate string field, id_str, alongside its numeric id for precisely this reason.

Fractional values fail differently for the same underlying reason: 0.1 + 0.2 evaluates to 0.30000000000000004, so monetary amounts carried as JSON numbers accumulate error through arithmetic. JSON has one number type and no decimal type, and the format cannot record the difference between an integer and a rounded double. If a value is an identifier or an amount of money, send it as a string and convert it deliberately at the boundary.

Values that change on the way out

The grammar has no token for NaN or Infinity — a number is an optional minus sign, digits, and an optional fraction and exponent. JSON.stringify does not throw on them, it substitutes: JSON.stringify({a: Infinity, b: NaN}) produces {"a":null,"b":null}, so a failed calculation arrives downstream as an innocuous null.

undefined behaves differently depending on where it sits. As an object value the key is removed entirely, so JSON.stringify({a: undefined, b: 1}) gives {"b":1}. As an array element it becomes null, so [undefined, 1] serialises to [null,1]. Negative zero flattens as well: JSON.stringify(-0) returns the string 0, and the sign is not recoverable.

Only BigInt fails loudly, throwing TypeError: Do not know how to serialize a BigInt. Python leans the opposite way and emits bare NaN and Infinity tokens from json.dumps by default — output that is not valid JSON and that JSON.parse rejects. Passing allow_nan=False makes it raise instead. Cross-language pipelines meet this problem from one side or the other.

No comments, no trailing commas, and the dialects that add both

There is no comment production in the grammar. Douglas Crockford has explained that he removed comments deliberately, because people had started using them to carry parsing directives, which would have fragmented the format. A trailing comma after the final member is likewise a syntax error, which is why adding one key to a configuration file produces a two-line diff.

Several dialects relax these rules, and none of them are JSON. JSONC — what VS Code settings and tsconfig.json actually use — permits comments. JSON5 adds comments, trailing commas, unquoted keys, single-quoted strings, and hexadecimal numbers. Both are close enough to pass a code review, and both make JSON.parse throw, so a tsconfig.json that opens perfectly in your editor will fail a naive parse in a build script.

It is also worth remembering that JSON has no date type. An ISO 8601 timestamp in a payload is a string by convention only; JSON.parse will never hand back a Date, and any conversion is code somebody wrote. If what you actually want is a configuration format with real comments, converting to YAML is more honest than fighting the parser.

Encoding rules and the __proto__ key

RFC 8259 section 8.1 requires JSON exchanged between systems outside a closed ecosystem to be encoded in UTF-8, and forbids implementations from adding a byte order mark. Parsers may ignore a leading BOM, and many do not — which is how a file that displays correctly still fails to parse.

Strings are sequences of Unicode code points, so it is possible to write an unpaired surrogate such as "�". Since ES2019, JSON.stringify emits escape sequences for lone surrogates instead of raw ill-formed output, so the result is valid UTF-8 and can be parsed again. Older engines produced bytes that a strict consumer would refuse.

One key deserves specific mention. JSON.parse creates an ordinary own property for __proto__ rather than setting the prototype, so parsing untrusted JSON does not pollute anything by itself. The risk is downstream: a recursive merge or a hand-rolled deep clone that copies that key with plain assignment moves it onto the prototype chain. Prototype pollution comes from the merge, not the parse. Finally, RFC 8259 allows any value at the top level where the older RFC 4627 required an object or array, so a bare 42 or "text" is a complete document that some long-lived validators still reject.

ThenCatch is a free developer toolkit. Learn more about us.