Why JSON runs almost everything, and why that's a problem
JSON is the one data format almost nobody thinks about until it breaks. It sits underneath every REST API response, every config file that isn't YAML, every localStorage blob, every webhook payload, every log line shipped to an aggregator, and every message passed between a frontend and a backend written in two completely different languages. It became the default not because it's the most powerful format available — XML has schemas and namespaces, Protocol Buffers are faster and smaller, YAML is more readable by hand — but because it's small enough to hold entirely in your head, maps almost directly onto native data structures in most programming languages, and has exactly one way to write anything, which makes "does this parse" a yes-or-no question instead of a matter of style.
That last property is the whole point, and it's also where most people's mental model of
JSON quietly diverges from what a parser actually enforces. Because JSON looks so much like
a JavaScript object literal, and because most developers' first contact with it is typing
{ name: "value" } into a JavaScript file, there's a widespread assumption that
JSON is basically "JavaScript object syntax, as a string." It isn't. JSON is a strict,
independently specified grammar — defined in RFC 8259
and maintained separately at json.org — that happens to
share a family resemblance with JavaScript object literals but enforces rules JavaScript
itself doesn't. No trailing commas. No comments. No unquoted keys. No single-quoted
strings. No functions, no undefined, no NaN. A JSON parser isn't
being pedantic when it rejects those things — it's following a specification that was
deliberately built to have zero ambiguity, at the cost of some of the convenience JavaScript
itself allows.
The gap between "looks like JSON" and "is valid JSON" is where almost every real-world JSON problem lives. A config file someone hand-edited and added a comment to. An API response that got truncated mid-stream and now ends on an unclosed brace. Two JSON objects concatenated together during a copy-paste without an array wrapper between them. A string value containing an unescaped quote character copied straight from a database. None of these look wrong to a human skimming the content. All of them are fatal to a parser, which doesn't skim — it reads character by character against a fixed grammar, and it either matches that grammar completely or it doesn't parse at all. There's no partial credit, no "close enough," no graceful degradation. JSON parsing is binary in a way most other parts of a web stack aren't, and that binary nature is precisely why it's both extremely reliable when correct and completely unforgiving when it isn't.
This guide is about closing that gap — understanding JSON at the level of what the grammar
actually permits, not what happens to work in whichever language and library you reach for
first, and building the habits that catch invalid, malformed, or subtly wrong JSON before
it becomes a 2 a.m. incident with a stack trace that just says Unexpected token
u in JSON at position 0 and nothing else useful.
The real anatomy of JSON — what the spec actually allows
JSON's entire grammar fits on a single page, and that's not an accident — it was designed that way. But "fits on a page" doesn't mean "obvious," and the exact boundaries of what's legal are where most confusion starts. There are exactly six data types in JSON, and nothing else exists. Understanding those six types precisely, and where they diverge from their equivalents in whatever language you're working in, prevents the majority of parsing surprises before they happen.
1. The six JSON value types, and nothing else
Every value in a JSON document is one of exactly six types: object,
array, string, number, boolean
(true or false), and null. That's the complete list.
There is no date type, no function type, no undefined, no NaN, no Infinity, no regular
expression, no binary data type, and no way to represent a set or a map distinct from a
plain object. Every one of those absences is a deliberate design decision, not an
oversight — JSON's author explicitly kept the type system minimal so that every
implementation, in every language, would agree on exactly what a given document means with
zero room for interpretation. The practical consequence is that anything richer than these
six types has to be encoded as a convention on top of JSON, not inside it: dates become
ISO 8601 strings, binary data becomes base64-encoded strings, and there's no way for the
format itself to tell you that's what happened — that context lives entirely outside the
JSON document, in whatever API contract or schema governs it.
2. Objects and the rules around keys
A JSON object is an unordered set of key-value pairs wrapped in curly braces, where every
key must be a string, and specifically a double-quoted string — not an identifier, not a
single-quoted string, not a number. {"1": "value"} is valid JSON;
{1: "value"} is not, even though many languages would treat those as
equivalent. Duplicate keys within the same object are technically not forbidden by the
grammar itself, but the specification explicitly leaves the behavior undefined when they
occur — different parsers resolve this differently, with most taking the last occurrence
and silently discarding earlier ones, which means a JSON document with duplicate keys can
parse successfully while producing a different result depending on which language and
library reads it. This is worth treating as effectively invalid in practice, even though a
validator might not flag it as a syntax error.
3. Arrays, and why order is the only thing they guarantee
A JSON array is an ordered list of values in square brackets, and the values inside it
don't have to be the same type — [1, "two", true, null, {"five": 5}] is
entirely legal. The only guarantee an array gives you is order; there's no fixed-length
requirement, no type consistency requirement, and no restriction on nesting depth defined
by the format itself (though individual parsers often impose their own practical depth
limits to avoid stack overflow on maliciously deep or corrupted input).
4. Strings, escaping, and the Unicode requirement
JSON strings must be wrapped in double quotes, never single quotes, and that rule has no
exceptions in the standard. Inside a string, a specific set of characters must be escaped
with a backslash — the double quote itself, the backslash character, and control
characters like newline (\n), tab (\t), and carriage return
(\r). A literal, unescaped newline character inside a JSON string is invalid,
even though it's easy to accidentally introduce one when building a JSON string manually
via concatenation rather than through a proper serializer. JSON strings are also required
to be valid Unicode, and the format includes a specific escape sequence,
\uXXXX, for representing any Unicode code point by its hexadecimal value —
which matters for characters outside the basic printable ASCII range, and is the reason
JSON can represent essentially any human language's text without a separate encoding
declaration the way HTML needs charset.
5. Numbers — and the type that causes the most cross-language pain
JSON has exactly one number type. Not integer and float as separate types, just
number, and the grammar defines its format precisely: an optional minus sign,
one or more digits, an optional fractional part after a decimal point, and an optional
exponent. Leading zeros are not allowed (0123 is invalid; 123 or
0.123 is fine). There is no hexadecimal notation, no octal notation, no
underscore digit separators the way some programming languages allow for readability, and
critically, the spec places no limit on precision or size — a JSON number can have as many
digits as you want. The practical problem is that JSON's unlimited-precision number is
usually deserialized into a language's native number type, and most languages' native
number types have real limits. JavaScript's Number type, for instance, can
only safely represent integers up to 2^53 − 1 before precision loss occurs — which is why a
large numeric ID (a Snowflake ID, a 64-bit database primary key) sent as a raw JSON number
can silently lose precision the moment JSON.parse() touches it, with no error,
no warning, just a slightly wrong number that might not surface as a bug until it's used to
look something up and fails to match.
6. What JSON has no concept of at all
It's worth listing explicitly what JSON simply cannot express, because these gaps are where
most "how do I represent X in JSON" confusion comes from. JSON has no comment syntax. JSON
has no date/time type. JSON has no way to represent a function, a class instance, or any
kind of executable code. JSON has no undefined — only null, and
the two are not interchangeable even though JavaScript conflates them in casual usage.
JSON has no way to represent NaN or Infinity, both of which are
legal JavaScript numeric values but have no JSON representation at all — attempting to
JSON.stringify() a value containing either of these in JavaScript silently
converts them to null, which is a frequently-missed source of silent data
loss. JSON also has no native way to represent binary data, dates, or sets — every one of
these needs a convention layered on top, agreed upon out-of-band between whoever produces
the JSON and whoever consumes it.
JSON.parse()
in a browser console, and through a permissive config-file parser that calls itself
"JSON-compatible." You'll frequently get three different outcomes on documents containing
comments, trailing commas, or single-quoted strings — not because any of them is "more
correct," but because only one of them is actually enforcing the RFC 8259 grammar, and the
other two are quietly accepting a superset for convenience. Knowing which one you're
actually relying on in production is the difference between code that works everywhere and
code that only works in the specific environment you happened to test it in.
How parsers actually read a JSON document, top to bottom
A conforming JSON parser doesn't scan the document looking for patterns — it walks the input one character at a time against a strict state machine defined by the grammar. It expects exactly one top-level value (an object, an array, a string, a number, a boolean, or null), and once that value's closing character is reached, anything else in the input after it is an error, not a second value to also parse. This is precisely why concatenating two JSON objects together — a common mistake when appending log entries or merging API responses naively — produces a parse error rather than "two objects": the format has no concept of multiple top-level values in one document, and if you need that, the answer is wrapping everything in a single top-level array, or using a genuinely different format like JSON Lines, which explicitly defines one JSON value per line as a related but distinct convention.
JSON Schema — the part of the ecosystem most people don't know exists
JSON itself has no concept of a "shape" a document must conform to beyond basic syntax — a JSON object with completely the wrong fields, wrong types, or missing required data is still perfectly valid JSON, because syntactic validity and structural correctness are different questions entirely. JSON Schema is the widely-adopted specification for describing that second layer: a JSON document (yes, itself written in JSON) that declares what fields are required, what type each field must be, allowed value ranges, string patterns, and nested object shapes for other JSON documents. It's the mechanism behind API request validation, IDE autocomplete for config files, and contract testing between services — and it's worth understanding as a genuinely separate concern from "is this JSON syntactically valid," because a document can pass one check and fail the other independently.
| Concept | What it actually governs | Common confusion |
|---|---|---|
| JSON syntax | Is this text parseable at all | Confused with "is this the right shape" |
| JSON Schema | Does the parsed data match an expected shape | Assumed to be part of core JSON; it's a separate spec |
| JSON Lines (JSONL) | One JSON value per line, for streaming/logs | Confused with a single JSON array of records |
| JSONC (JSON with Comments) | A superset some tools accept (tsconfig.json, VS Code settings) | Assumed to be standard JSON; strict parsers reject it |
Common mistakes that quietly break production systems
JSON mistakes share a specific personality: they're almost always invisible in a formatted, pretty-printed view, and only become obvious in the raw string a machine actually receives. These are the errors behind the majority of "it works on my machine" and "the API just started failing for no reason" investigations.
Mistake #1: Trailing commas copied from JavaScript habits
Modern JavaScript happily allows a trailing comma after the last item in an array or object
literal — {"a": 1, "b": 2,} — and most editors and linters don't just tolerate
it, they actively encourage it for cleaner diffs. That leniency does not exist in JSON. A
trailing comma anywhere in a JSON document is a hard syntax error under RFC 8259, full
stop. This is, by a wide margin, the single most common reason hand-written or
hand-edited JSON fails to parse, precisely because it's a habit so deeply ingrained from
writing actual JavaScript that it happens without conscious thought, and it's invisible on
a quick visual scan of formatted output.
Mistake #2: Single quotes instead of double quotes
JavaScript treats single and double quotes as interchangeable for strings. JSON does not — the specification requires double quotes for every string, including object keys, with no exception. Code that builds a JSON-like string manually using single quotes, or JSON pasted from a source that used single-quoted Python-style dictionaries, produces something that looks unmistakably like JSON to a human and is rejected outright by any conforming parser.
Mistake #3: Unquoted or improperly quoted object keys
Because JavaScript object literals allow unquoted identifier keys —
{name: "value"} is completely valid JavaScript — it's an extremely common
mistake to write the same thing expecting it to also be valid JSON. It isn't. Every key in
a JSON object must be a double-quoted string, with no exceptions for keys that happen to
look like valid identifiers. This mistake and the trailing-comma mistake together account
for the overwhelming majority of "I copied this straight out of my JavaScript code and it
won't parse as JSON" reports.
Mistake #4: Leaving comments in what's supposed to be strict JSON
Because tools like tsconfig.json, VS Code's settings.json, and
several other widely-used config files support comments despite the .json
extension, there's a common and reasonable-seeming assumption that JSON comments are
universally supported. They are not part of the JSON specification at all. Those specific
tools are parsing a superset informally called JSONC, with their own custom parser that
tolerates comments before handing the result to standard JSON-consuming code. Copy a
commented config block into a context that uses a strict, spec-compliant JSON parser — a
different tool, a different language, an API request body — and it fails immediately, with
no indication that "comments aren't allowed here" is the actual problem, because the error
message just points at a character position.
Mistake #5: Sending large integers that silently lose precision
Because JSON's number type has no size limit but most consuming languages parse JSON
numbers into a fixed-precision native type, a large integer — a 64-bit database ID, a
Snowflake-style distributed ID, certain financial or scientific values — can be perfectly
valid JSON and still come out wrong on the other end. In JavaScript specifically, any
integer larger than 2^53 − 1 risks silent precision loss the moment it passes through
JSON.parse(), with no thrown error and no warning; the number just becomes a
different, nearby number. The standard fix is encoding large IDs as JSON strings rather
than JSON numbers specifically to sidestep this — a convention many major APIs (including
several large social and financial platforms) adopted after being burned by exactly this
issue in production.
Mistake #6: Assuming NaN, Infinity, and undefined survive a round trip
All three of these are legitimate values inside a JavaScript program, and none of them
exist in JSON. Calling JSON.stringify() on an object containing
NaN or Infinity silently converts those values to null
rather than throwing an error, and object properties whose value is undefined
are dropped from the output entirely rather than being preserved as anything. Both
behaviors are specified and consistent, but they're also silent — nothing in the console
tells you data was altered or dropped during serialization, which means a bug caused by
this can survive undetected for a long time if nothing downstream specifically checks for
the values that went missing.
Mistake #7: Concatenating JSON payloads without an array wrapper
A JSON document has exactly one top-level value. Code that appends new records to a file or
stream by writing {...}{...}{...} one after another — common in naive logging
implementations — is not producing valid JSON at all, even though each individual
{...} block is fine in isolation. A standard JSON parser reading that file will
successfully parse the first object and then error on unexpected content immediately after
it. The two legitimate fixes are wrapping everything in a single top-level array
([{...}, {...}, {...}]), which requires the file to be rewritten as a whole
each time something is appended, or switching to the JSON Lines convention, which
explicitly defines the format as one complete JSON value per line and is built specifically
for the append-only, streaming use case that plain concatenated JSON is being misused for.
Mistake #8: Unescaped special characters inside string values
A string value containing a literal double quote, backslash, or raw newline character that hasn't been escaped will break parsing at exactly that point, and this is a particularly common problem with data pulled from a database or user input and inserted into a JSON string via naive concatenation rather than a proper serializer. A user's bio field containing a quotation mark, a file path containing backslashes on Windows, or a multi-line text field copied in with its raw newlines intact are all realistic, everyday sources of this exact failure — and because the invalid character is buried inside what looks like a normal, short string value, it's one of the harder classes of JSON error to spot by eye in a large payload.
Mistake #9: Encoding issues from mismatched charset assumptions
The JSON specification requires Unicode text, and modern JSON is overwhelmingly UTF-8 in practice, but that alignment isn't automatic — a JSON payload generated by a system assuming a different encoding (Latin-1, Windows-1252) and then read by a system assuming UTF-8 can produce garbled characters or outright parsing failures on any non-ASCII content, without any error clearly pointing at "encoding mismatch" as the cause. This surfaces most often with names, addresses, and free-text fields containing accented characters, curly quotes, or non-Latin scripts, where the payload parses "successfully" but the actual text content is corrupted — arguably worse than an outright parse failure, because it fails silently rather than loudly.
Mistake #10: Deeply nested or circular structures that break serialization
JSON's grammar places no formal limit on nesting depth, but real parsers do, specifically to avoid stack overflow from either legitimately deep data or maliciously crafted input designed to exhaust resources — which means "technically valid per the spec" and "actually parses in your runtime" can diverge on sufficiently deep structures. Separately, and more commonly encountered, attempting to serialize an object containing a circular reference (an object that references itself, directly or through a chain) throws an error in most implementations rather than producing infinite output, because JSON has no way to represent a reference or pointer — every value has to be fully, literally written out, and a circular structure has no finite literal representation.
Mistake #11: Trusting a formatted, pretty-printed view while debugging
This is less a JSON-specific technical mistake and more a workflow mistake that makes every other mistake on this list harder to catch: debugging by looking at JSON after it's been run through a pretty-printer or a browser's formatted network tab view, rather than the raw string that was actually transmitted. Pretty-printers are usually built on top of a parser that already successfully parsed the document — which means if the raw payload was actually malformed, you may never see it in its broken form at all, because the tool either silently repaired it, or failed before rendering anything and left you looking at a blank panel with no indication of why. Checking the literal raw response body — via curl, a Postman raw view, or a validator that operates on the actual string — is the only way to see what a parser downstream is actually going to receive.
Expert-level tricks for validating, parsing, and debugging JSON
This is where the real, compounding gains live — the specific habits and techniques that separate someone who occasionally fights with a JSON parse error from someone who catches the problem in seconds and knows exactly why it happened.
1. Validate the raw string, never the formatted view
Whatever tool you're using — a browser's network tab, an API client, a log viewer — always find the option to view the literal, unformatted response body, and validate that exact text. A formatted view has usually already been through a successful parse-and-reformat cycle by the tool showing it to you, which means it can hide the very problem you're trying to diagnose. When something "looks fine but won't parse," the raw string is almost always where the actual answer is sitting, usually as a single character you'd never spot by eye in a reformatted view.
2. Learn to read the position number in a parse error, don't just react to the message
A JSON parse error like Unexpected token , in JSON at position 47 looks
cryptic, but that position number is a precise, literal character offset into the exact
string you handed the parser — not a vague estimate. Counting to that character (most
editors show cursor position, or a quick script can slice the string) reliably lands you
at or extremely close to the actual problem, almost every time, and turns a frustrating
guessing exercise into a five-second lookup. This habit alone eliminates most of the time
people spend "trying to find" a JSON error by scanning visually.
3. Treat JSON.stringify()'s replacer and indentation arguments as debugging tools, not just formatting
JSON.stringify(value, replacer, indent) takes a second argument that can
filter or transform values during serialization, and a third that controls indentation for
human-readable output. Passing a replacer function is a genuinely useful debugging
technique for finding exactly which property in a large object is causing a serialization
failure or holding an unexpected type — logging each key and value as the replacer visits
it narrows down a problem inside a deeply nested object far faster than manually inspecting
the whole structure by eye.
4. Default to encoding large integers, timestamps, and binary data as strings
Rather than relying on every consumer of your JSON to handle large numbers, dates, and binary data correctly, encode them defensively at the source: large IDs as quoted strings rather than raw numbers to sidestep precision limits entirely, timestamps as ISO 8601 strings rather than a numeric epoch value whose unit (seconds vs. milliseconds) is a constant source of cross-team confusion, and binary data as base64-encoded strings, which is the universal, dependency-free way to represent arbitrary bytes inside JSON's text-only format. This shifts the burden from "every consumer must handle this edge case correctly" to "the data was never ambiguous to begin with."
5. Use JSON Lines instead of concatenated JSON for anything append-only
If a use case involves writing records one at a time over time — logs, event streams, exported datasets too large to hold in memory as one array — reach for the JSON Lines convention (one complete, valid JSON value per line, newline-delimited, no enclosing array and no commas between lines) rather than either a single ever-growing JSON array that has to be fully rewritten on every append, or naively concatenated JSON objects that don't parse as a single document at all. JSON Lines is specifically designed for this pattern, is trivially readable line-by-line without loading the whole file into memory, and is widely supported by log processors and data pipelines exactly because of that streaming property.
6. Validate structure with JSON Schema, not just syntax
Syntactic validity and structural correctness are different guarantees, and conflating them
is a common source of bugs that pass every "is this valid JSON" check while still being
completely wrong for the application consuming it — a required field missing, a string
where a number was expected, an enum value outside the allowed set. Defining a JSON Schema
for anything that crosses a service boundary — an API request body, a config file, a
webhook payload — and validating incoming data against it catches an entire category of
"technically valid JSON, functionally wrong data" bugs before they reach application logic,
with an error message that names the specific field and constraint that failed rather than
a downstream undefined is not a function several layers removed from the
actual cause.
7. Reach for a proper parser, never regex, for anything beyond the most trivial extraction
JSON's grammar is recursive and context-sensitive — a comma means something different
inside a string versus inside an array, and nesting depth is unbounded — which makes it
fundamentally unsuited to reliable regex-based parsing or extraction, despite how tempting
a quick regex feels for "just grabbing one field" out of a JSON blob. A string value can
legally contain characters that look exactly like structural JSON syntax
({"note": "the value is {escaped} here"}), and a regex has no way to
distinguish structural characters from string content the way a real parser's state
machine does. Every language has a built-in, spec-compliant JSON parser at this point;
using it is never the slow or heavyweight option compared to regex, it's simply the correct
one.
8. Keep a mental (or literal) checklist for the "looks right, isn't" failure modes
Because the mistakes covered earlier are so visually subtle, the fastest debugging workflow
for "why won't this parse" is running down a short, fixed checklist before doing anything
else: trailing comma, single quotes, unquoted keys, a stray comment, an unescaped quote or
backslash inside a string value, a concatenated double top-level document, or a
NaN/undefined/Infinity value that got serialized
somewhere upstream. The overwhelming majority of real-world JSON failures are one of these
exact things, and checking them in order is almost always faster than staring at the
payload waiting for the error to jump out visually.
9. Pipe API responses and config files through a validator as a standing habit, not a last resort
The teams that stop losing time to JSON errors are the ones who validate as a default step — pasting a new API response or a hand-edited config file through a proper validator before wiring it into code, rather than only reaching for a validator after something has already failed in an unclear way. This is a five-second habit that converts an unpredictable amount of future debugging time into a fixed, small, upfront cost, and it's particularly valuable the moment a payload was touched by a human (hand-edited, copy-pasted, assembled via string concatenation) rather than produced entirely by a serializer that guarantees valid output by construction.
10. Understand what your specific language's parser tolerates beyond the spec, and don't rely on it
Some parsers are stricter than the spec requires, and some are more lenient — a handful of JSON5 or JSONC-aware tools will happily accept comments, trailing commas, or unquoted keys that a strict RFC 8259 parser would reject outright. Relying on that leniency because "it works in my dev environment" is a common way to ship JSON that silently breaks the moment it reaches a different language, a different library version, or a stricter production parser. Writing to the strict common denominator, and validating against that standard rather than against whatever your local tool happens to accept, is what keeps JSON portable across the entire stack it's likely to travel through.
| Symptom | Usual cause | Fix |
|---|---|---|
| Parses locally, fails in production | Local tool is more lenient than the spec (JSON5/JSONC-like) | Validate against strict RFC 8259, not your editor's tolerance |
| Large ID comes back wrong after parsing | Integer exceeds safe precision for the numeric type | Encode large IDs as strings |
| Fails only on some records, not others | Unescaped quote/backslash/newline inside string values | Serialize properly instead of concatenating strings |
| "Unexpected token" at position 0 | Empty response body, or HTML error page returned instead of JSON | Log and inspect the raw response before parsing |
| Works for one record, breaks on append | Concatenated JSON objects with no array wrapper | Use a top-level array or switch to JSON Lines |
Putting it all together
JSON rewards precision, not familiarity. The format looks so close to a JavaScript object literal that it's easy to treat it as one and only discover the difference at the worst possible moment — mid-deploy, mid-integration, mid-incident. The goal isn't memorizing every edge case in this guide; it's internalizing the one idea underneath all of them: JSON is a strict, literal, unambiguous grammar, and every mistake covered here is really just a different way of accidentally relying on leniency the specification never actually grants.
If you take one habit from this guide, let it be this: validate the raw string, not the formatted view, and do it before wiring new data into code rather than after something breaks. A malformed JSON payload doesn't announce itself — it sits there looking completely normal in a pretty-printed panel while quietly being unparseable to whatever machine receives it next. The fix isn't more careful hand-editing; it's building validation into the workflow every single time a payload crosses a boundary you don't fully control.
JSON also rarely travels alone in a real stack — the same payload that needs validating often also needs its binary fields base64-decoded, its embedded tokens decoded and inspected, its dynamic values checked against a pattern, or its structure minified before it ships. If that's where you are, the same underlying principle applies across every one of those adjacent tools: know exactly what format you're actually looking at before you trust it or transform it at scale.