Why JSON Became the Default
JSON did not win because it is the most elegant design. It won because it sits at the right point between "enough" and "simple". A comparison of the three mainstream interchange formats:
| Dimension | JSON | XML | YAML |
|---|---|---|---|
| Size | Small (no redundant tags) | Large (tag pairs) | Smallest |
| Data types | 6, enough | Requires XSD | Rich, with type inference |
| Comments | ❌ Not supported | ✅ | ✅ |
| Parse cost | Very low (native) | High (DOM/SAX) | Medium (indentation-sensitive) |
| Hand-authoring | Medium (brackets are error-prone) | Low | High |
| Home turf | Web APIs, config, logs | Enterprise/finance legacy | K8s, CI, IaC |
The takeaway: use JSON for high-frequency machine-to-machine exchange, and YAML for config files humans maintain by hand. That is exactly why Kubernetes manifests are YAML while its API traffic is JSON.
When you need to check data types and escaping rules on the fly, keep the JSON cheat sheet open in a second tab.
JSON Syntax: Where It Is Strict
The entire JSON grammar is only a handful of rules, but every one is hard — there is no error tolerance.
Six Hard Rules
- Keys must use double quotes:
"key", neverkeyor'key' - String values must use double quotes, not single quotes or backticks
- No trailing commas: no comma after the last element
- No comments: both
//and/* */cause a parse failure - Encoding must be UTF-8 (mandated by RFC 8259)
- The top level may be an object, array, string, number,
true/false, ornull— not just an object
Six Data Types
| Type | Example | Gotcha |
|---|---|---|
| string | "hello" |
Double quotes required; escape control characters |
| number | 42, 3.14, 1e10 |
No NaN, Infinity, leading zeros, or hex |
| boolean | true / false |
Lowercase only |
| object | {"a": 1} |
Keys must be strings |
| array | [1, 2, 3] |
Element types may be mixed |
| null | null |
Not undefined, not NULL |
Valid vs Invalid
| Snippet | Verdict | Why |
|---|---|---|
{"a": 1} |
✅ | — |
{a: 1} |
❌ | Key not quoted |
{'a': 1} |
❌ | Single quotes |
{"a": 1,} |
❌ | Trailing comma |
{"a": 01} |
❌ | Leading zero |
{"a": NaN} |
❌ | No such literal in JSON |
{"a": undefined} |
❌ | No such type |
[1, "2", null] |
✅ | Arrays may mix types |
Formatting vs Minifying: Not an Aesthetic Question
How Much Minifying Saves
Stripping all indentation and newlines typically cuts 30–50% of the payload. Where key names repeat heavily (say ten thousand records of the same shape), gzip or Brotli widens the gap further. A 10 MB JSON can shed 3–5 MB of transfer — several seconds on a weak mobile connection.
When you need to minify, use a JSON minifier rather than a hand-rolled regex that strips whitespace; a regex will corrupt spaces inside string values.
Indentation Conventions
| Style | Where it is used |
|---|---|
| 2 spaces | Most common — the default in the JS/TS ecosystem (Prettier, ESLint) |
| 4 spaces | Python ecosystem, some Java projects |
| Tab | Rare, and incompatible with YAML — not recommended |
What matters is consistency within one repository. Inconsistency hurts more than the choice itself.
Two Counter-Intuitive Points
- Formatting before signing breaks verification. HMAC, digital signatures, and JWTs are computed over the raw byte sequence; one extra space changes the result completely.
- Do not print beautified large objects into production logs. Print the minified form and truncate by length, or log volume and write latency both run away.
Validation: Catch Errors Before Runtime
JSON.parse often throws nothing more than Unexpected token } in JSON at position 1234, which looks useless. Knowing the distribution lets you locate it in seconds.
The Five Most Common Errors
| Error type | Typical message | Actual cause |
|---|---|---|
| Trailing comma | Unexpected token } |
Extra comma after the last item |
| Single quotes | Unexpected token ' |
Copied from a JS literal |
| Unclosed bracket | Unexpected end of JSON input |
Missing } or ], reported at end of file |
| Comments | Unexpected token / |
Someone wrote a // note |
| BOM | Unexpected token at position 0 |
File starts with |
The Locating Heuristic
Look backwards from the reported position, not forwards. A parser only throws at the character it cannot continue from, so the typo is usually a few lines earlier. Concretely:
- Error at the end → almost certainly an unclosed bracket; count pairs from the outside in
- Error pointing at a comma → usually an extra trailing comma, not missing content
- Error at position 0 → check BOM and encoding first
- Error mid-file → look at the end of the previous line
Pasting the raw content into a JSON formatter is the fastest first move; most implementations mark the offending line and column directly.
JSON Schema: Turning Structure Into an Executable Contract
Validating "is this legal JSON" only settles syntax. Validating "are the fields and types right" requires JSON Schema.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["id", "email", "createdAt"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "format": "uuid" },
"email": { "type": "string", "format": "email" },
"age": { "type": "integer", "minimum": 0, "maximum": 150 },
"status": { "enum": ["active", "suspended", "deleted"] },
"createdAt": { "type": "string", "format": "date-time" }
}
}
Commonly Used Keywords
| Keyword | Purpose |
|---|---|
type |
Type constraint; may be an array like ["string", "null"] |
required |
List of mandatory fields |
enum |
Enumerated allowed values |
minimum / maximum |
Numeric range |
minLength / maxLength |
String length |
pattern |
Regex match (debug it with a regex tester) |
additionalProperties |
Set to false to reject undeclared fields |
format |
Semantic format (email/uri/uuid/date-time, etc.) |
Inferring a Schema From Real Responses
Hand-written schemas drift from real data. The sturdier approach is to generate a first draft from an actual response and then tighten it by hand — that is what a JSON Schema generator is for. Afterwards, tighten additionalProperties and mark which fields may be null; only then is it a usable contract.
Running it in CI with ajv surfaces structural breakage from API changes before merge.
Large Files and Streaming
Memory Is Not "As Big As the File"
This is the most commonly misjudged point: JSON in memory after parsing typically occupies 3–10x the original text. Every key and value becomes a separate object or string with pointer and metadata overhead. A 100 MB JSON file can easily need 500 MB–1 GB of RAM for a naive JSON.parse.
| File size | Recommended approach |
|---|---|
| < 10 MB | Just JSON.parse; no optimisation needed |
| 10–100 MB | Stream it, or process in batches |
| > 100 MB | Streaming mandatory; consider newline-delimited JSONL |
| > 1 GB | Switch to a columnar or binary format (Parquet, Arrow) |
Streaming Parsers
| Library | Language | Notes |
|---|---|---|
simdjson |
C++ with bindings | SIMD-accelerated; GB/s-class parsing |
ijson |
Python | Lazy iteration, emits as it reads |
JSONStream |
Node.js | Incremental extraction by JSONPath |
serde_json StreamDeserializer |
Rust | Zero-copy friendly |
Extracting Fields With JSONPath
If you only need a few fields out of a large file, do not parse the whole tree — target them with a JSONPath expression and discard irrelevant branches as you go. You can tune the expression against real data with the JSONPath finder.
Comparing Structures: What Actually Changed
After changing config or upgrading an API, eyeballing two JSON documents almost always misses something. Compare the parsed structure instead of the text:
- Added or removed keys → check
requiredand compatibility - Changed values → check whether the type drifted (
"1"vs1) - Reordered arrays → decide whether order carries business meaning
JSON Diff gives you structured differences, far more reliable than textual line diffs, which drown in formatting noise.
Security: Four Real Risks When Parsing JSON
| Risk | Impact | Fix |
|---|---|---|
Parsing with eval() / new Function() |
Remote code execution, total compromise | Always use JSON.parse |
| Prototype pollution | Deep-merge mutates every object's prototype | Strip __proto__/constructor/prototype before merging |
| Deep nesting stack overflow | Recursive parser crashes; denial of service | Enforce a depth limit before parsing |
| Large-integer precision loss | Money and order ids silently become wrong | Transmit fields above 2^53-1 as strings |
Number two is the sneakiest:
const payload = JSON.parse('{"__proto__":{"isAdmin":true}}');
const config = {};
merge(config, payload); // Dangerous: pollutes Object.prototype
console.log({}.isAdmin); // true — every object in the app is affected
Number four is the most damaging in finance and ordering: 9007199254740993 parses to 9007199254740992, and nothing throws.
Task Cheat Sheet
| Task | Approach | Tool |
|---|---|---|
| Read a one-line minified JSON | Beautify with 2-space indent | JSON formatter |
| Track down a parse error | Paste it in and read the line/column | JSON formatter |
| Shrink a payload | Strip whitespace | JSON minifier |
| Browse a deep structure | Tree viewer | JSON viewer |
| Diff two config revisions | Structural diff | JSON Diff |
| Infer a schema from a response | Generate, then tighten | JSON Schema generator |
| Pull fields from a big file | JSONPath expression | JSONPath finder |
| Debug a field regex | Validate the pattern first | Regex tester |