Try the JSON Formatter

Why JSON Parses Differently in JavaScript, Python, and Go — Precision, Duplicates, and Parser Security

JSON's specification deliberately leaves number precision, duplicate key handling, and Unicode surrogate pairs as implementation-defined — which is why identical JSON produces different results in JavaScript (integers lose precision above 2^53), Python (arbitrary precision integers), and Go (float64 by default). Here's the parser differential security attack from duplicate keys, prototype pollution via JSON merge, and why NDJSON enables streaming gigabyte datasets with constant memory.

July 13, 2026 7 min read
Share: Facebook WhatsApp LinkedIn Email
Why JSON Parses Differently in JavaScript, Python, and Go — Precision, Duplicates, and Parser Security

JSON's official specification (ECMA-404 and RFC 8259) deliberately defines a minimal format — no comments, no trailing commas, no date literals, no binary type — and every extension to JSON that adds these features breaks interoperability with standard parsers, which is why the decision to use JSON5 or JSONC in a project has implications that extend beyond the immediate file being edited

The previous articles on this site covered JSON vs YAML vs MessagePack vs Protocol Buffers, JSON Schema and API contracts, structured logging, large number precision, and why JSON doesn't allow comments. This article addresses JSON parsing behaviour across implementations — specifically the subtle differences between JSON parsers in different languages that cause identical JSON text to produce different results.


Parsing number precision: the IEEE 754 problem

JSON has no specification for number precision. The spec says numbers are sequences of digits with optional decimal point and exponent — but doesn't specify how large or precise they can be.

What actually happens varies by parser:

JavaScript JSON.parse(): converts all numbers to IEEE 754 64-bit double-precision floats. Maximum safe integer: 2^53 − 1 = 9,007,199,254,740,991. Any integer above this is rounded to the nearest representable float. As covered in the large number article: JSON.parse('9007199254740993')9007199254740992.

Python json.loads(): integers are parsed as Python int (arbitrary precision). json.loads('9007199254740993')9007199254740993 (correct). Floats are parsed as Python float (IEEE 754 double).

Java's Jackson library: by default, all numbers without decimal points parse as long (64-bit signed integer, max 2^63 − 1). This is better than JavaScript but still limited. Jackson can be configured to parse large integers as BigInteger.

Go's encoding/json: numbers decode to float64 by default when the target type is interface{}. When the target type is known (struct field of type int64), integers parse correctly. The use of json.Number preserves the original string representation.

The interoperability implication: if a backend (in Python or Java) generates a JSON response containing a 64-bit integer ID (like Twitter's tweet IDs, which are 64-bit), and a JavaScript frontend parses it, the ID loses precision. Twitter's API returns IDs both as integers and as strings ("id_str") specifically because of this problem.


Unicode in JSON: escape sequences and actual Unicode

JSON requires that strings contain valid Unicode. String values can be expressed two ways:

Direct Unicode: "café" — the é character appears directly as UTF-8 bytes in the JSON file.

Unicode escape sequences: "caf\u00e9" — the é is expressed as a JSON escape sequence.

Both are equivalent per the JSON spec, and any compliant parser must accept both. But implementations differ:

Output escaping: when serializing, some parsers escape all non-ASCII characters to \uXXXX sequences; others output UTF-8 directly. Python's json.dumps() defaults to ensure_ascii=True (escaping non-ASCII); ensure_ascii=False outputs UTF-8 directly. Both are valid JSON.

Surrogate pairs for characters above U+FFFF: characters outside the Basic Multilingual Plane (emoji, many rare Unicode characters) are represented in JSON with surrogate pairs: \uD83D\uDE00 for 😀. Some parsers handle surrogate pairs correctly; others don't, producing \uFFFD (replacement character) or errors.

Control characters: JSON requires that control characters (U+0000 to U+001F) be escaped. Tab must be \t, newline \n, carriage return \r. Some parsers silently accept literal control characters in strings; strict parsers reject them.


Duplicate keys: what actually happens

JSON's specification says object names should be unique but doesn't require it — "SHOULD" not "MUST" in RFC 8259 terminology. The spec says implementations can handle duplicate keys in any of several ways.

What different parsers do:

JavaScript JSON.parse(): last-wins. JSON.parse('{"a":1,"a":2}'){a: 2}.

Python json.loads(): last-wins by default. json.loads('{"a":1,"a":2}'){'a': 2}. Custom object_pairs_hook allows handling all pairs.

Java's Jackson (default): last-wins.

Go's encoding/json: last-wins.

jq command-line tool: processes all keys, last-wins for final output.

The security implication: if application code relies on parsing a JSON key for security decisions, and an attacker can control JSON input, they can insert a duplicate key — then the parse result depends on which key wins. If the security check reads one version and the application logic reads another (in a system that doesn't use the same parser for both checks), a bypass may result. This is called "JSON parser differential" — a class of security vulnerability.


JSON streaming and the newline-delimited format

Standard JSON parsers load the entire document into memory before returning a result — fine for small documents, problematic for large datasets:

JSON.parse() in JavaScript: loads the entire string into memory. A 500 MB JSON file requires 500 MB of memory for the string plus additional memory for the parsed object.

Streaming parsers: process JSON token-by-token without loading the full document. Libraries like stream-json (JavaScript), ijson (Python), and jsonstreaming (Go) allow processing the 100th element of a million-element array without holding all previous elements in memory.

JSON Lines (NDJSON — Newline-Delimited JSON): a convention where each line is a separate, complete JSON document:

{"id":1,"name":"Alice","email":"[email protected]"}
{"id":2,"name":"Bob","email":"[email protected]"}
{"id":3,"name":"Charlie","email":"[email protected]"}

Why NDJSON enables streaming: each line is independently parseable. A streaming reader processes one line at a time, keeping only one record in memory. This enables processing gigabyte-scale datasets with constant memory usage.

Use cases for NDJSON: log files (each log entry is one line), large dataset exports, event streams, batch API responses.


The __proto__ pollution attack: parser security

Prototype pollution is a JavaScript-specific JSON parsing vulnerability:

In JavaScript, JSON.parse('{"__proto__": {"isAdmin": true}}') doesn't mutate Object.prototypeJSON.parse() is specifically protected. But libraries that naively merge parsed JSON objects into plain objects using obj[key] = value patterns can be vulnerable:

const parsed = JSON.parse('{"__proto__": {"isAdmin": true}}');
const merged = Object.assign({}, parsed);
// merged.__proto__ is still Object.prototype — safe

// But a naive merge:
function merge(target, source) {
  for (const key of Object.keys(source)) {
    target[key] = source[key];  // Sets target.__proto__ if key is "__proto__"
  }
}

The lodash _.merge() vulnerability (CVE-2019-10744): a prototype pollution vulnerability in lodash's merge function allowed attackers who could control JSON input to set arbitrary properties on Object.prototype, affecting all objects in the application. This was patched in lodash 4.17.12.


How to use the JSON Formatter on sadiqbd.com

  1. Debugging serialisation differences: format two JSON documents from different sources (Python backend vs JavaScript frontend) to compare visually — differences in number precision, Unicode escaping, and key ordering become apparent
  2. Minify for transmission: the minify option removes all whitespace, reducing payload size for API transmission — useful when manually constructing API test payloads
  3. Validate before parsing: the formatter validates JSON syntax before formatting — if it fails, the error message and position help locate syntax issues in machine-generated JSON that may have encoding or generation bugs

Frequently Asked Questions

Why do JSON parsers sometimes produce different results for the same valid JSON input? Because the JSON specification deliberately leaves several behaviours implementation-defined. The spec doesn't specify number precision (beyond "implementations must handle numbers"), doesn't require duplicate key rejection (only "should" be unique), doesn't specify what to do with surrogate pairs or embedded control characters in strings, and doesn't specify the ordering of object keys in output. Each parser implementation makes its own choices within these gaps. For most common JSON (integers under 2^53, ASCII strings, unique keys, no control characters), parsers produce identical results. The differences emerge at the edges — which is exactly where automated systems and security-relevant code need consistent behaviour.

Is the JSON Formatter free? Yes — completely free, no sign-up required.

Try the JSON Formatter free at sadiqbd.com — format, minify, and validate JSON instantly.

Share: Facebook WhatsApp LinkedIn Email

JSON Formatter

Free, instant results — no sign-up required.

Open JSON Formatter →
Similar Tools
UUID Generator Hash Generator URL Encoder/Decoder Regex Tester Random String Generator JSON Diff Cron Explainer Password Generator
JSON Schema Validation and API Contracts: OpenAPI, Contract Testing, and Validation Libraries
Developer
JSON Schema Validation and API Contracts: OpenAPI, Contract Testing, and Validation Libraries
Structured JSON Logging: How to Debug Production API Errors and Search Logs at Scale
Developer
Structured JSON Logging: How to Debug Production API Errors and Search Logs at Scale
Why 9007199254740993 Becomes 9007199254740992: JSON Numbers and JavaScript's Precision Limit
Developer
Why 9007199254740993 Becomes 9007199254740992: JSON Numbers and JavaScript's Precision Limit
Why JSON Doesn't Allow Comments — and the JSON5/JSONC Variants That Do (and Why They Break Standard Parsers)
Developer
Why JSON Doesn't Allow Comments — and the JSON5/JSONC Variants That Do (and Why They Break Standard Parsers)