It starts with a network request
Somewhere around 3 a.m., on a project that is already running late, you open DevTools and refresh the page. Under the Network tab, a request you didn't remember writing finishes in a green flash. You click it. In the Response tab sits what looks like a wall of text: braces and brackets, quoted keys, a thousand lines with no indentation to speak of. There is no friendly XML tag in sight, no schema, no instructions. Just data, stacked like shipping containers, readable if you squint.
You copy it into a scratch file, run it through a formatter, and suddenly it makes sense. An object here, an array there, a couple of booleans. You could trace the whole shape with your eyes in about twenty seconds.
That wall of text is JSON. If you have spent any time writing software in the last two decades, you have almost certainly produced one, consumed one, or debugged one at an hour you would rather not remember. It is the de facto language of machines talking to each other — the common tongue of one service asking another for a user record, a price, a forecast, a list of what's in stock. And it's worth understanding properly, not just vaguely, because it is everywhere, it has sharp edges, and it has been quietly winning for a quarter of a century.
What JSON actually is
JSON stands for JavaScript Object Notation. The acronym is accurate in the narrowest possible sense: the format's syntax was lifted straight from JavaScript's object literals. Douglas Crockford, who popularized it and wrote the first specification around 2001, took the way JavaScript expresses data — the { key: value } shape, the brackets for lists, the quote style for strings — and froze it into a language-agnostic format. He named it, described it on json.org, and essentially gave it to the world with a blessing: no fee, no ceremony, just a permissive license.
The trick Crockford pulled off was making a format that both humans and machines could read without fighting each other. XML, the incumbent heavyweight at the time, was designed for documents. JSON was designed for data. That distinction, more than any technical detail, is what carried it. A JSON file opens in a text editor and reads like a structure you already know; it doesn't demand a stylesheet, a namespace declaration, or a DTD before it will deign to be useful.
Its formal standardization went through the usual stages. ECMA published it as ECMA-404. The IETF eventually blessed it as RFC 8259, replacing the older RFC 7159, which had itself replaced the original RFC 4627 from 2006. Today, if you want the boring truth in legal-grade prose, RFC 8259 is the canonical text. json.org still hosts the grammar in a few one-page diagrams, which is a nice reminder of how small the whole thing really is.
A note on the name, because it trips people up: JavaScript Object Notation is something of a historical accident. Apart from the syntax lineage — the fact that it was born from JavaScript's object literals — JSON has nothing to do with JavaScript. It has no functions, no dates, no methods, no inheritance, none of the machinery that makes JavaScript a language. A Go program, a Rust program, and a Python program all read and write it without a shred of JavaScript anywhere in the stack. The name is unfortunate, a little like calling the metric system "French Units." We're stuck with it.
Why it beat XML
To understand why JSON won, you have to remember what the world looked like before it. In the late 1990s and early 2000s, XML was the answer to every question, and web services were built on SOAP. A single request meant writing an envelope, a header, a body, namespaces for every vocabulary you could dream up, and then a stylesheet (XSLT) to turn the response into something a human could read. Config files were not much better. Everything was angle brackets stacked on angle brackets, and the attributes-versus-elements debate was a genuine religion with denominations. Should the name be <name> or <person name="...">? People argued. Conference talks were given. Careers were made.
Then along came something you could write down on a napkin. JSON maps one-to-one onto the data structures that every programming language already has. The object is your dictionary, your hashmap, your associative array. The array is your list. Strings, numbers, booleans, null — all of them exist in every language worth using. There was no impedance mismatch to design around, no clever abstraction to learn. You looked at a JSON document and you already knew how to read it, because you had seen its shape in a million places before.
The killer feature was that you didn't need a schema to make sense of it. XML's great selling point, the schema that would let you validate and transform documents, turned out to be its great burden in practice. A SOAP endpoint might ask you to reference a WSDL, a stylesheet, a namespace or three. JSON just showed up and handed you a payload, and you could parse it with code you wrote in an afternoon. The learning curve was effectively zero. You didn't read a spec to understand JSON; you looked at one example and got it.
It isn't that JSON is beautiful. Nobody looks at a wall of braces and calls it art. JSON won because everything else was more annoying. It was a relief, not a revelation. And that turned out to be enough. By the time the REST movement gained steam and everyone started building public APIs, JSON was the default, XML was the legacy requirement, and a whole generation learned web development never having written a single SOAP envelope.
The six data types
The entire language fits on an index card. There are six data types, and that's the whole grammar, full stop. No more, no fewer.
An object is a collection of key-to-value pairs wrapped in curly braces. The keys are strings; the values can be anything, including other objects.
{
"name": "Ada",
"role": "admin"
}
An array is an ordered list of values wrapped in square brackets.
["north", "east", "south", "west"]
A string is a sequence of Unicode characters enclosed in double quotes, with a small set of escape sequences for the awkward cases.
"This is a string with a \"quote\" in it and a line break.\n"
A number is an integer, a decimal, or an exponent form — 42, 3.14159, 6.022e23, -273.15. No leading zeros, no NaN, no Infinity. Numbers are strictly finite.
A boolean is one of two lowercase words, exactly as written: true or false. Capital True is a syntax error. TRUE is a syntax error. JSON does not grade your effort.
And null, a keyword meaning "no value here," distinct from an empty string and distinct from an absent key. null is a value that exists and says nothing.
{
"name": "Marvin",
"nickname": null,
"age": 42,
"online": false,
"aliases": ["Marv", "M", "The Paranoid Android"]
}
That example, a single object, touches every type except the exponent number. Real documents are just bigger versions of the same trick, nested until you run out of indentation. A user record from an API, a product in a catalog, a log line from a server — all of them are objects and arrays holding strings, numbers, booleans, and nulls.
{
"user": {
"id": "a1b2c3",
"username": "jcarver",
"joinedAt": "2021-04-12T18:30:00Z",
"verified": true,
"stats": {
"posts": 412,
"followers": 89,
"ratio": 4.63
},
"tags": ["go", "cli", "embedded"],
"lastKnownIp": null
}
}
Everything in JSON is ultimately one of those six things. There is no type for dates, no type for bytes, no type for money. Those are all conventions you layer on top. That's part of why JSON survived: it refuses to invent a type system, and so every language can adopt it without translation.
The sharp edges of the syntax
Simple as it is, JSON has more footguns than its index-card reputation suggests. Most of them come from the fact that JSON is stricter than the JavaScript it was born from.
Keys must be double-quoted. { name: "Ada" } is not JSON, and neither is { 'name': 'Ada' }. JSON strings take double quotes only; single quotes are not an acceptable substitute. This is the first thing that bites people coming from JavaScript, Python, or almost any modern language, where quotes are a matter of taste.
No comments. Not //, not /* */, not #. The specification doesn't just disallow them; the grammar has no place for them at all. If you need to annotate a config file, you glue on a tool, or you accept the silence. This is the most complained-about rule in the entire format, and we'll come back to it.
No trailing commas. [1, 2, 3,] and {"a": 1,} are both errors. When you're hand-editing JSON and adding one item to a list, you will fix the same missing-comma-or-trailing-comma bug until the day you retire.
Numbers are picky. A leading zero is illegal, so 042 is out, and 0.42 is in. The .5 shorthand is out; you need 0.5. Exponents are fine, 1e3 and 1E-2 both pass. And -0 is a legal, if philosophically strange, number that most parsers hand back as negative zero.
Duplicate keys are technically legal. {"a": 1, "a": 2} parses without error. What happens then is undefined by the spec — most parsers keep the last value, some keep the first, a few throw. Since the behavior is unspecified, any code that relies on it is gambling.
Unicode is handled through \uXXXX escapes, with four hex digits for BMP characters. Anything outside the Basic Multilingual Plane, like an emoji, can be written directly as the literal character (always legal) or, when using \u escapes, must be expressed as a pair of surrogate code units: "😀" is the grinning face. The full escape set is short and fixed: \", \\, \/, \b, \f, \n, \r, \t, and \u. That's all of them. There is no \0, no \x, no \e. Try any of those and the parser will look at you with quiet disappointment.
The practical upshot: JSON tolerates almost no sloppiness, which is exactly why it's so predictable. It's not a format that forgives; it's a format that either parses or doesn't, and there's no third option.
Is JSON a subset of JavaScript?
Early on, RFC 4627 described JSON as a JavaScript subset. That was always a little awkward, and the modern spec stepped carefully away from the claim. Here's the thing: a JavaScript object literal is a superset of JSON, but the two are not the same language.
JavaScript will happily accept a pile of things JSON forbids. Single-quoted strings. Trailing commas. Comments. Functions as values. undefined. NaN. Infinity. new Date(). None of that is JSON. Conversely, there are things JSON permits that early JavaScript engines tripped on. The two line separator characters U+2028 and U+2029 are legal inside JSON strings, but old JavaScript engines treated them as actual line breaks, which would quietly break an embedded JSON string. It took until ES2019 for JavaScript to align. So "JSON is a subset of JavaScript" turned out to be the sort of claim that's true on a good day and embarrassing on a bad one.
Why does the distinction matter in practice? Because of what JSON.parse does and doesn't accept. When you call JSON.parse on a payload, you are not evaluating code; you are invoking a proper parser that only understands the six types and the strict grammar. JSON.parse('{a: 1}') throws. JSON.parse("{'a': 1}") throws. JSON.parse('{"a": function(){}}') throws. If you treat JSON as "the same as an object literal with extra steps," you will be wrong, loudly, in production.
The cleaner way to think about it: JSON is its own language, small and closed, that happens to share a visual family resemblance with JavaScript. The resemblance is where the format came from, but the format has outgrown the comparison.
Where you meet it every day
JSON is the background radiation of modern computing, so let's name the places where it actually shows up, because the list is longer than you'd guess.
REST APIs are the big one. Request bodies, response bodies, sometimes query parameters encoded as JSON. You send it, you receive it, the whole web of public and internal services runs on it. When you call the GitHub API, when you hit Stripe, when your mobile app talks to your backend, it's JSON on the wire more often than not.
Config files. package.json is JSON, which means every Node project on Earth declares its dependencies and scripts in it. tsconfig.json, .eslintrc, .babelrc, composer.json, manifest.json for browser extensions and PWA manifests — the pattern is everywhere. Tools keep picking JSON for configuration because it's the one format every language can read without a bespoke parser library.
Browsers. localStorage stores strings, and the standard move is to JSON.stringify your state into it and JSON.parse it back out. IndexedDB stores structured clones, but JSON is still the lingua franca for anything you need to serialize and ship around the page. The fetch API returns bodies you parse as JSON with one line.
Databases. PostgreSQL has a jsonb type, and you can query into it with -> operators. MongoDB's default storage format, BSON, is a binary variant of JSON that trades human-readability for speed of scanning and traversal. Redis, in its less disciplined moments, holds a lot of JSON-looking strings. Even SQLite gained a json_ family of functions.
JSON Lines, or NDJSON, is JSON's answer to streaming: one JSON value per line, no enclosing array. It's how a lot of logs are emitted, how data pipelines stream rows to each other, how big datasets get exported in a format you can tail like a log file. Each line is independently valid, so you can process a file without loading it into memory.
WebSockets carry JSON messages as a matter of course. Server-Sent Events push JSON payloads. Even the humble clipboard, when you copy a table out of a spreadsheet into certain tools, becomes a JSON structure.
The thread through all of this is the same one that beat XML: JSON is the shape your data already has, and so it's the least-friction way to move that shape from one system to another.
Parsing it in different languages
Every language of consequence ships JSON support, and it's instructive to see how the flavor differs. These aren't tutorials; just a taste of each.
In JavaScript it's built in, and it's two functions: JSON.stringify to serialize, JSON.parse to read.
const text = JSON.stringify({ id: 7, tags: ["a", "b"] });
const obj = JSON.parse(text);
Python's json module is the standard library workhorse: json.loads and json.dumps, with json.dump and json.load for files.
import json
with open("config.json") as f:
data = json.load(f)
print(data["name"])
In Java, you almost always reach for a library — Jackson or Gson are the classics — because the JDK's story here has always been clumsy. A Gson one-liner maps JSON straight onto a typed class:
User user = new Gson().fromJson(body, User.class);
Go's encoding/json is another story entirely. Because Go is statically typed, the idiomatic path is to define a struct, tag it with field names, and let the package do the mapping:
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
Rust goes one further with serde, which isn't a JSON library so much as a serialization framework that JSON happens to be one face of. You derive Serialize and Deserialize on your types, and serde_json fills in the rest. The compiler checks the whole arrangement at build time, which means the first time your JSON shape changes, you find out when the code refuses to compile, not when the pager goes off at 2 a.m.
The pattern across all of them is the same: text in, structure out. The differences are just where the language's type system puts the friction.
Security: lessons paid in blood
JSON's early history is a security object lesson, and it's worth retelling because the scars shaped how we parse data today.
The original sin was parsing untrusted JSON with eval. In the early 2000s, when JSON was young and the tooling didn't exist, the fast way to read a payload in a browser was eval("(" + json + ")"). The problem: eval executes code, and a payload that looks like JSON can carry more than data. A string like {"msg":"hi"}["constructor"]["constructor"]("alert('owned')")() is valid JavaScript: the first constructor lands on Object, the second resolves to Function, and the quoted string becomes the body of a function that runs on the spot. Parsing untrusted data with eval is remote code execution with extra steps. The rule that eventually sank in — never eval JSON, always use a real parser — is not paranoia; it's a lesson from people who got owned.
JSONP was the era's workaround for a different problem, and it was its own compromise. Before CORS, browsers blocked cross-origin reads, so people wrapped JSON in a callback and loaded it as a <script> tag: jsonpCallback({...}). A script tag can be loaded cross-origin, so you'd get your data. But you were, again, executing code supplied by a remote server in your page's origin. If that server was compromised, or lied, it could run anything. JSONP worked until it didn't, and modern APIs have largely abandoned it in favor of CORS and a hard rule against raw cross-origin script execution.
The top-level array was a subtle cross-site risk. The classic same-origin rule is that browsers send cookies even on cross-origin requests, but scripts can't read the response. Arrays, however, are legal JavaScript expressions, and in older engines a hostile page that overrode the Array constructor could capture the values from a top-level JSON array it loaded as a script. The practical fix was to wrap the data in a top-level object — {"balance": 999999} — since a top-level {...} in a script is parsed as a block, not as an expression an attacker could evaluate. It was never written into the RFC: RFC 8259 actually permits any JSON value at the top level, a bare array included. But the habit stuck, and you'll still find security checklists and API guides telling designers to avoid returning bare top-level arrays.
Then there's prototype pollution. When a parser merges an object into a JavaScript object's prototype chain, keys like __proto__ can quietly poison every object in the runtime. A payload containing {"__proto__": {"isAdmin": true}} can, in vulnerable libraries that merge objects naively, escalate privileges or break application logic. The fix is in how libraries merge, not in JSON itself — but you learn to be suspicious of any parser or deep-merge utility that assigns keys without checking for __proto__.
And JSON has a few brute-force denial-of-service tricks of its own. A "JSON bomb" is a small file that expands enormously — nesting arrays and objects to the point that parsing it consumes memory far out of proportion to its size. There are canonical examples that are a few kilobytes and expand to gigabytes of parsed structures. Deeply nested payloads can blow the call stack of recursive parsers. The practical defenses are unglamorous: use a real parser, never eval, cap the size of what you accept, cap the depth you'll parse, and avoid merge-based libraries that write into prototypes.
The through-line of all of it: JSON is data, and the moment you treat it as code, or the moment you let it allocate without bound, you are trading away the safety that a format only appears to offer.
Validation and extensions: Schema, JSON5
Because JSON has no schema, a whole ecosystem grew up around validating it. JSON Schema is the best-known attempt: a JSON document that describes other JSON documents, with keywords for required fields, types, patterns, minimums, and items (for arrays), and so on. It has a web-ish history of draft versions and varying implementations — a validator that passes one draft may choke on another — but the idea is sound. You write a schema, you feed documents to a validator, and you learn, before the first request ever hits a database, that age is a string when it should be a number.
Then there are the pragmatic extensions people actually reach for when strict JSON is too annoying. JSON5 loosens the syntax: comments, trailing commas, single quotes, unquoted keys, plus Infinity, NaN, and hex numbers. It's a superset of JSON, so any valid JSON is valid JSON5, and it's a genuinely nicer way to hand-write config files. JSONC is a looser idea — JSON with comments — that appeared in Visual Studio Code's config files and spread by familiarity. HJSON, "Human JSON," is another flavor with comments, multiline strings, and a relaxed syntax.
Here's the honest caveat: none of these are standards. JSON5 has a spec and a following, but it is not RFC-anything. JSONC isn't even a well-defined thing; it's a convention that VSCode made popular and everyone half-understands. They are pragmatic hacks, invented because the real thing has no comments and people who write config files by hand got tired of it. If you use them, you're trading strict, boring, universally supported JSON for something a parser you control can read — and that's a fine trade, as long as you remember that the whole world's tools won't necessarily read it back.
Its limits and the complaints
A format that has been this successful still gets a steady stream of complaints, and most of them are legitimate.
The big one is comments, and it's the one people feel. Configuration files are where humans write JSON by hand, and hand-written files are where you want to explain yourself. JSON has no place to put the explanation. So projects either invent a surrounding format, or they live with the silence, or they quietly adopt JSON5 and pretend the problem is solved. Every team that has ever maintained a package.json with a hundred dependencies and a mysterious build flag has wanted, at least once, to write a comment.
There's no date type. This is the wound every language re-injures. A date in JSON is a string, and the convention is ISO 8601, but nothing enforces it. So you get "2024-03-15", and "2024-03-15T10:30:00Z", and the unlucky "03/15/2024" from the system nobody updated, and then the parsing code grows a library like date-fns or Joda just to reconcile the formats everyone invented anyway. The result is that "parse a date" is never one line in a JSON-adjacent stack; it's a dependency.
Number precision is the quiet disaster. JSON numbers are IEEE-754 doubles. That's a binary floating-point representation with roughly 15 to 17 significant decimal digits. It cannot represent big integers faithfully, which is a problem when the integers in question are IDs or, worse, amounts of money. A 64-bit ID, or a bank balance that needs every digit exact, will round-trip through a double and come back subtly wrong. Teams working with financial data or large identifiers learn the workaround — send the number as a string — and then the API docs carry a sad little asterisk about it.
There's no binary. If you need to ship a file, an image, or a blob of bytes, you base64-encode it, which inflates it by about 33 percent, and you send it as a string. It works. It's also an ugly tax on every payload that carries real bytes.
Deep nesting can blow the stack. Recursive parsers, and they're the common kind, run out of call depth on hostile or accidental nesting. It's a robustness problem the format itself doesn't address.
Duplicate keys exist and do something undefined. Numbers are floats whether you wanted that or not. And because the format is deliberately minimal, every application reinvents the same wheels — timestamp formats, enum conventions, error shapes — with the same tiny variations that guarantee the same tiny incompatibilities.
None of these complaints killed JSON. They just made everyone aware, repeatedly, that JSON is a low bar that happens to be exactly where we all decided to stand.
The alternatives
A quick, honest tour of the formats that compete with JSON is in order, because each one was invented to fix the specific complaints above, and each one pays for it somewhere else.
YAML is the human-friendly option: indentation-based, comments native, no braces, no quotes for most keys. It reads like a well-organized notes file, and for configuration it's a genuine pleasure — which is why Kubernetes, Docker Compose, and CI pipelines are full of it. But YAML's simplicity is a lure. The spec is enormous and subtle; yes and no have historically been booleans in some parsers, anchors and aliases do reference tricks, and the infamous "Norway problem" (a date-like string parsed as a date) has bitten more than one deployment. YAML 1.1 and YAML 1.2 even disagree with each other about basic scalars. For data exchange between machines, YAML is the wrong tool; for humans writing config, it's often the right one, with the caveat that "it's just JSON without the braces" is a lie.
TOML is the config-focused option. It's built for tables of key-value pairs — [section] headers, key = "value", comments, dotted keys, native date support. It dodges most of YAML's ambiguity because its syntax is deliberately small, and it has found its home in exactly the places where JSON's missing comments hurt most: Cargo.toml, pyproject.toml. The trade: it's config-shaped, not data-shaped. You wouldn't ship a query result as TOML.
Protocol Buffers is the compact, typed option. You define a schema in a .proto file, run a code generator, and get efficient binary messages with required fields, enums, and versioning baked in. Google's internal services have run on it for years. In exchange for speed and smallness, you pay with a build step, a schema you can't skip, and a developer experience where "just look at the payload" is no longer possible — the wire format is opaque. When you're serving millions of requests and every byte and every CPU cycle counts, protobuf earns its keep. For a casual API, it's overkill.
MessagePack and CBOR are binary JSONs: same data model, compact encodings, no schema, no codegen. A number is a few bytes instead of a few characters. They're lovely when bandwidth is precious and you want to stay schema-free. BSON, MongoDB's format, is another binary variant, optimized for speed of scanning rather than raw compression, and its dates and ObjectIds fill some of JSON's gaps.
The honest rule of thumb: JSON wins when humans and machines both need to read it, when you want zero ceremony, or when you're building anything that might outlive you. YAML and TOML win when a human writes it by hand. Protobuf wins when you control both ends and you need to move a lot of bytes very fast. The binary JSONs win when you need JSON's shape without JSON's weight. Everything else is a judgment call.
Closing: boring formats live long
JSON has been around for about twenty-five years now, which in software is roughly several geological eras. It's been declared dead more times than is polite to count, in favor of YAML, or protobuf, or GraphQL, or a dozen things that came and went. And it's still here, because it won on the axes that matter most: it's good enough, it's everywhere, and it's so simple that the spec fits in a pamphlet.
"Good enough" is doing more work in that sentence than it looks like. JSON is not the best format for any single job. It's not the most compact, not the most expressive, not the fastest to parse, not the friendliest to hand-write. But it is the one that every language, every database, every browser, every logging pipeline, and every developer you will ever hire already speaks. In the ecosystem lottery, ubiquity beats elegance every time, and JSON bought ubiquity early and cheap.
It's also still the right default for a surprisingly large share of new problems. When you're designing an API, reaching for JSON is not a failure of imagination. It's the choice that means your payload can be read by a curl, inspected in DevTools, mocked in a test, logged by a proxy, and understood by a colleague who joined last Tuesday. The moment you reach for something cleverer, you are accepting a cost in tooling and comprehension, and you should only pay that cost when you know exactly what you're buying.
When should you reach for something else? When the bytes matter enough to justify a binary format. When a schema and compiled types are worth a build step. When humans write the file by hand and the comments hurt enough to adopt TOML or a JSON variant. When the numbers must be exact. All real reasons, all worth respecting.
But the quiet lesson — the one that outlasts the format itself — is that boring formats live long. XML was the exciting, ambitious, standards-blessed technology. JSON was the boring one that worked. Every engineer who has rewritten a system twice already knows the shape of this story. The next format that replaces JSON, if one ever does, will probably be just as unglamorous. It will be the thing that is almost as good as everything else and slightly easier to use, and it will win for exactly the reasons JSON did: not because it's beautiful, but because it's the path of least resistance, and there is a certain quiet dignity in being the default.
So the next time you open DevTools at 3 a.m. and a wall of braces scrolls past, you'll know exactly what you're looking at, why it looks the way it does, and why it's going to be waiting for you in the morning.
