JSON is text that looks like a JavaScript object — {"name":"Ada","age":30} — and it's how most of the web talks: fetch an API, get JSON, JSON.parse to a JavaScript object; save settings to package.json, also JSON. This beginner's guide explains syntax (6 value types, no single quotes), how to read an API response, validate trailing commas, and convert JSON to CSV — no prior JSON needed.
- What: JavaScript Object Notation — UTF-8 text, not binary — 6 value types: string
"hi", number42, booleantrue/false, null, object{}, array[]; specified by json.org and RFC 8259. - Syntax: keys must be
"quoted"strings, no single quotes, no trailing comma, no comments, noundefined—{"name":"Ada"}not{name:'Ada',}. - Run the web:
fetch("/api/user")→ JSON{"user":{"id":42,"name":"Ada"}}→data.user.namein JS,data["posts"][0]in Python; configpackage.json, tsconfig.json, OpenAPI, GeoJSONare all JSON. - Tools: paste into a JSON formatter validator to highlight line 3 col 18 error and pretty-print, and into a convert JSON to CSV to turn
[{"name":"Ada","age":30}]→name,age\nAda,30with correct quoting. - Quick test:
JSON.parse('{"a":1}')→ object;JSON.stringify({a:1})→ text — the two directions you use daily.
What Is JSON — Text That Looks Like a JavaScript Object (But Isn't Code)
JSON (JavaScript Object Notation) is plain text — you can open data.json in Notepad and read it — that uses the same literal syntax as JavaScript objects and arrays but strictly as text, not code to execute. That makes it human-readable ("name": "Ada"), machine-parseable in every language, and safe (no function calls).
It was standardized as ECMA-404 and RFC 8259. Example:
{"name": "Ada Lovelace", "age": 30, "verified": true}
That's a UTF-8 text file with 38 bytes — not binary like an image. APIs, config, and storage all use the same form.
Where You Meet JSON Every Day
- APIs (REST):
fetch("https://api.example.com/user/42")→ body is JSON →JSON.parse(text)→data.name. Nearly every public API defaults to JSON; see MDN: JSON. - Config:
package.json(npm),tsconfig.json, OpenAPIopenapi.json, GeoJSON for maps — all JSON. - Storage: browsers
localStoragestores JSON stringified; NoSQL (MongoDB) logs JSON.
JSON vs XML: {"name":"Ada"} (21 bytes) vs <name>Ada</name> (23+ with tags) — JSON is lighter, faster, and native to JavaScript (JSON.parse built-in) while XML needs a DOM parser. That's why JSON won the web (XML still in SOAP).
Text, Not Binary — Open It Anywhere
Save the snippet above as person.json (UTF-8) → open in Notepad → you see {"name": "Ada"}. Change "Ada" → "Bob" and save — still valid JSON if quoting and commas hold. Try: echo '{"a":1}' | python -m json.tool pretty-prints in terminal.
Syntax — Objects {}, Arrays [], and 6 Value Types Only
Everything is one of six:
- String — double-quoted
"Ada"(not'Ada'), escapes\", \n, \t, \uXXXX. - Number —
30,3.14,-42,1e10— no leading zero0123, noNaN/Infinity. - Boolean —
trueorfalselowercase. - Null —
null(noundefined). - Object —
{"key": value, ...}unordered name/value pairs — name must be string. - Array —
[1,2,3]ordered list.
{
"name": "Ada", // string
"age": 30, // number
"active": true, // boolean
"spouse": null, // null
"tags": ["js", "json"], // array of strings
"address": { "city": "London" } // nested object
}
Rules: keys must be "quoted" strings — {name: "Ada"} is JavaScript but not JSON → parser: Unexpected token n. No trailing comma — {"a":1,} → Unexpected token }. No single quotes — {'a':1} → Unexpected token '. No comments — // comment not allowed (use JSON5 if you need comments). No undefined — use null.
Dates Are Strings
JSON has no Date type — dates are ISO strings: {"created": "2024-01-01T00:00:00Z"} → parse with new Date(data.created). Storing "2024-01-01" bare without quotes → Unexpected token 2 (parser sees minus).
Types Deep — Strings Must Be Quoted, Numbers Only, Null Not Undefined
Beginner errors, with correct next to it:
// Wrong → Error at line:col // Right
{"name": 'Ada'} // ✗ single quotes {"name": "Ada"}
{"age": 30,} // ✗ trailing comma {"age": 30}
{name: "Ada"} // ✗ unquoted key {"name": "Ada"}
{"active": undefined} // ✗ undefined not allowed {"active": null}
{"date": 2024-01-01} // ✗ bare date {"date": "2024-01-01T00:00:00Z"}
Strings win for big ints: JavaScript numbers lose precision beyond 2^53-1 (9007199254740991). API that returns {"id": 9007199254740993} round-trips as 9007199254740992 → use {"id": "9007199254740993"} as string, keep as BigInt.
Real Example — API Response You Actually See
{
"user": {
"id": 42,
"name": "Ada Lovelace",
"verified": true,
"tags": ["admin", "editor"]
},
"posts": [
{ "id": 1, "title": "Hello JSON", "views": 1234 },
{ "id": 2, "title": "Second post", "views": 567 }
]
}
Read: top level object → user is object → tags is array of two strings; posts array of 2 objects. Access: JS data.user.name → "Ada Lovelace", data.posts[0].title → "Hello JSON"; Python data["user"]["name"], data["posts"][0]["title"]. This shape repeats everywhere: fetch → response.json() (does JSON.parse for you) → object. See MDN Fetch + JSON.
In the wild: package.json with "dependencies": {"react": "^18.0.0"} → nested object where keys are package names.
Nested and Arrays — Flatten for CSV
Nested: {"address":{"city":"London","zip":"W1"}} → for CSV, flatten to address_city="London" with dot or underscore, because CSV has no nesting. See conversion next.
How JSON Runs the Web — Fetch, Config, and Storage
Three places JSON is glue:
- Fetch API:
fetch("/api/user/42").then(r => r.json()).then(data => console.log(data.user.name))—r.json()parses. JSON.parse and JSON.stringify are built-ins. See Fetch. - Config:
tsconfig.json{"compilerOptions": {"target": "ES2020"}}→ TypeScript reads object. Invalid JSON (trailing comma) →TS18002: Expected ','. - Storage:
localStorage.setItem("user", JSON.stringify(user))→ retrieveJSON.parse(localStorage.getItem("user"))— storage is string-only, JSON bridges object→string. Even logs are often JSON lines ({"level":"info","msg":"request"}) for parsers.
Compare to YAML: YAML allows comments and unquoted keys but indentation-sensitive — many projects keep JSON for strict interchange, YAML for human config. Both convert.
Parse and Stringify — The Two Directions You Use Daily
// Text → Object (receive)
const text = '{"name":"Ada","age":30}'
const obj = JSON.parse(text) // { name: "Ada", age: 30 }
console.log(obj.name) // Ada
// Object → Text (send)
const user = { name: "Ada", age: 30 }
const json = JSON.stringify(user) // '{"name":"Ada","age":30}'
fetch("/api/user", { method: "POST", body: json, headers: { "Content-Type": "application/json" } })
// Pretty for humans: 2-space indent
JSON.stringify(user, null, 2)
// {
// "name": "Ada",
// "age": 30
// }
JSON.stringify drops undefined, function, Symbol — {a: undefined, b: () => {}} → {}. Use null instead. See MDN stringify replacer.
Validate and Format — Fix Trailing Commas and Quotes in One Paste
Paste minified {"a":1,"b":2,} → validator says Unexpected token } at line 1 col 13 — remove trailing , → valid. Paste {'a':1} → Unexpected token ' → replace ' with " and quote key.
Paste into a JSON formatter validator to highlight line 3 col 18 error, auto-fix quotes, and pretty-print with 2-space indent — no guessing which comma. Tip: also check echo '{"a":1}' | python -m json.tool in terminal.
Schema Validation — Beyond Syntax
Syntax valid doesn't mean shape correct — {"age": "thirty"} is valid JSON but wrong type. Use JSON Schema (json-schema.org) or Zod/Joi to validate age is number, name is string at parse.
Convert — JSON ↔ CSV (One Array of Objects = One Table)
An array of flat objects is a table: [{"name":"Ada","age":30},{"name":"Bob","age":25}] ↔ CSV:
name,age
Ada,30
Bob,25
Rules: header row = keys, rows = values, quoted if comma/newline/quote inside. Nested: flatten {"address":{"city":"London"}} → address_city=London with dot/underscore, because CSV has no nesting. Paste array into a convert JSON to CSV → choose delimiter comma vs semicolon (EU Excel) → download CSV — it handles quoted commas and newlines correctly per RFC 4180 CSV.
When to Choose Which
JSON for APIs/config/nesting, CSV for spreadsheets/tables. Convert when analyst wants Excel: API JSON → CSV for pivot.
Common Pitfalls — 5 Before You Commit Valid JSON
| Mistake | Fix |
|---|---|
| Single quotes | 'Ada' → "Ada" |
| Trailing comma | {"a":1,} → remove before } or ] |
| Unquoted key | {name:"Ada"} → {"name":"Ada"} |
| Comments | Use JSON5 or strip — JSON has no // |
| Date bare | {"d":2024-01-01} → {"d":"2024-01-01T00:00:00Z"} |
Strings, Numbers, and Booleans — Rules That Trip Beginners
Strings: double-quoted only, escapes \", \\, \/, \b, \f, \n, \r, \t, \uXXXX. Example {"quote": "She said \"hi\""} → value She said "hi". Single quote fails: {'a':1} → parser error. Unescaped newline inside string fails — use \n.
Numbers: 42, 3.14, -0.5, 1e10 allowed — no leading zero 0123 → Unexpected number, no NaN or Infinity (use null or string "NaN"). Leading 0 only as 0 or 0.5.
Booleans and null: lowercase only true/false/null — True, False, NULL, NONE fail. No undefined — JSON.stringify({a: undefined}) drops key → {"b":1} not {"a":null}.
Real Workflow — Fetch, Validate, Store, Convert (End-to-End)
- Fetch:
const res = await fetch("https://api.example.com/users"); const data = await res.json(); // does JSON.parse— checkres.okbefore parse, or catchUnexpected token <when API returned HTML error, not JSON. - Validate: paste response into formatter validator → highlights line 1 col 1 if HTML — fix endpoint. Check type:
if (typeof data.age !== 'number') throwbefore using — syntax valid doesn't mean shape correct. - Store:
localStorage.setItem("user", JSON.stringify(user))→ retrieveJSON.parse(localStorage.getItem("user") || "null")— storage is string-only, JSON bridges object↔string. Without stringify,setItem("user", user)stores[object Object]. - Convert: need Excel? Array
[{"name":"Ada","age":30}]→ CSVname,age\nAda,30via converter — delimiter per RFC 4180, quote commas/newlines correctly. Nested flatten as above.
Pretty vs Minified — When Each Matters
// Minified (wire): {"user":{"id":42,"name":"Ada"}}
// Pretty (human): {
// "user": {
// "id": 42,
// "name": "Ada"
// }
// }
JSON.stringify(user) // minified, no spaces — smaller for network
JSON.stringify(user, null, 2) // pretty, 2-space indent — committed config, logs
Minified saves bytes on wire (APIs); pretty is for committed package.json, tsconfig.json and logs — diff friendly. Formatter pretty-prints minified in one paste before you read it.
JSON5, JSONC, and Comments — When Pure JSON Is Too Strict
Pure JSON per RFC 8259 forbids comments and single quotes — {"a":1} // comment fails. Two supersets allow them for config:
- JSON5: single quotes, trailing commas, comments
//—{'a':1, // comment }valid → used in some configs. - JSONC (JSON with Comments): VS Code's
tsconfig.jsonandsettings.jsonallow//and trailing commas — editor parses, interchange must be pure.
Rule: store config as JSONC for humans if your tool supports it, but send pure JSON over network — strip comments before fetch or API will 400. See JSON5.
1) Keys quoted? {"name":1} not {name:1}
2) Trailing comma? {"a":1,} → remove before } or ]
3) Single quotes? {'a':1} → "a"
Paste into validator after each fix — line:col tells exact char.
JSON Schema — Syntax Valid ≠ Shape Correct
{"age": "thirty"} passes parser but fails app (age should be number). JSON Schema validates shape after syntax:
{
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "number", "minimum": 0 }
},
"required": ["name", "age"]
}
Validate with json-schema validator or Zod in JS: z.object({name: z.string(), age: z.number()}).parse(data) — throws before you use data.age + 1 on string. Syntax is line 1, schema is line 2 — both needed.
Escaping, Unicode, and Big Numbers — What parse Does
- Unicode:
{"city": "M\u00FCnchen"}is valid JSON —\u00FCdecodes toü→München.JSON.parsehandles\uautomatically. Don't double-escape. - Big numbers:
9007199254740993loses in JS (becomes9007199254740992) — store as string"9007199254740993"and useBigIntor string compare. - Duplicate keys:
{"a":1,"a":2}is valid JSON per spec but behavior undefined — last wins in JS. Avoid.
Security — Don't Trust JSON You Didn't Validate
JSON from user or third-party is untrusted — parsing alone doesn't sanitize. Example attack: {"__proto__": {"isAdmin": true}} proto pollution if you merge blindly via Object.assign — use Object.create(null) or check __proto__. Log injection: {"msg": "hi\n[error] fake"} with newline inside string → log parser splits line — validate \n handling. Always validate shape before use, and never eval JSON — use JSON.parse only. See OWASP Deserialization.
Performance and Size — Minify, Gzip, and Streaming
APIs send minified (no spaces) + gzip (JSON compresses well — 70% typical). For large arrays (100K rows), don't JSON.parse 10MB at once — stream via JSONStream or NDJSON ({"id":1}\n{"id":2}) per line. Pretty print for humans, minify for wire, stream for large. See MDN stringify space param.
NDJSON vs JSON Array
[{"id":1},{"id":2}] needs full parse; {"id":1}\n{"id":2} (NDJSON) parses line-by-line for logs (jsonl) — choose array for small, NDJSON for logs/stream. See ndjson.org.
1) echo '{"a":1,}' | python -m json.tool # fails trailing , → fix
2) cat data.json | jq . # pretty + validate + query
3) JSON.stringify(obj, null, 2) → diff friendly committed config
Use jq to query: cat api.json | jq '.user.name' → Ada.
History and Why JSON Won — Douglas Crockford to RFC 8259
JSON was formalized by Douglas Crockford in 2001 as a subset of JavaScript literal syntax, then standardized via json.org and RFC 8259 (2017, obsoletes RFC 7159). It won because it's what JS already did — JSON.parse is native, no XML DOM, no custom parser. Every language added json.loads (Python), JSON.parse (JS), json.Unmarshal (Go) with same text. For interchange, dumb text beats smart binary (MessagePack) on debuggability — you open it in Notepad.
When JSON Is Not the Answer — YAML, TOML, CSV
JSON is great for machine interchange, weaker for human config where comments and trailing commas matter. YAML allows # comment, unquoted keys, multiline | — good for Kubernetes manifests but indentation-sensitive (2-space trap). TOML allows comments and tables — good for Cargo.toml. CSV is for flat tables, not nesting. Choose: API/storage → JSON; human config with comments → JSONC/YAML; table → CSV via converter above.
Reading Real Errors — Line 3 Col 18 Means What
Validator reports: SyntaxError: Unexpected token } at line 3 col 18 → count chars: line 3 is "age": 30, // comma before } on next line → col 18 is the } after , — remove comma. Another: Unexpected token ' at col 2 → you used ' not ". Fix, paste again → Valid JSON, 3 keys.
// config.js — fail fast
import { readFileSync } from 'fs'
const raw = readFileSync('config.json', 'utf8')
try {
const cfg = JSON.parse(raw)
if (typeof cfg.port !== 'number') throw new Error('port must be number')
} catch (e) {
console.error('Invalid config.json:', e.message)
process.exit(1)
}
Catch line:col before service starts — not after deploy.
jq and Command Line — Query JSON Without Code
Command line JSON via jq (like grep for JSON):
cat api.json | jq '.user.name' # → "Ada"
cat api.json | jq '.posts | length' # → 2
cat api.json | jq '.posts[] | .title' # each title
echo '{"a":1}' | jq . # pretty + validate
jq is the fastest way to check "is this valid and what key is null" before opening an editor. Alternative: python -m json.tool.
Common Questions — Top and Nested Access
Top level must be object or array — {"a":1} or [1,2], not bare 1 alone (some parsers allow, RFC prefers container). Nested access: data["address"]["city"] if key has dot. Dynamic key: data[keyVar] not data.keyVar.
Keep one source: committed pretty JSON (2-space) for diffs, wire minified for size — formatter toggles both.
Record Keeping — Version and Validate in CI
Commit pretty JSON (2-space) and validate on CI: python -m json.tool config.json > /dev/null && echo ok || fail or jq empty config.json. Pre-commit hook: fail if .json invalid before push. Keep package.json sorted — formatter keeps keys order.
Version your openapi.json like code — git diff on pretty shows which endpoint added age type change.
Copy working JSON block as template — one correct {"name":"Ada"} reused beats four hand-typed variants with different quote mistakes.
Keep one source: committed pretty for diffs, wire minified for size — formatter toggles both in one paste.
Bonus: keep package.json sorted and 2-space — npm install already does; diff then shows real change, not reformat noise.
Validate early, fail fast at boot — not at 3am when JSON.parse on bad file crashes request.
Keep one source: committed pretty for diffs, wire minified for size — choose per context, not per file.
Copy working {"a":1} template — one correct reused beats four hand-typed with different trailing commas.
Version your JSON like code — git diff on pretty shows which key was added when age changed.
Keep one source — committed pretty for diffs, wire minified for size — choose per context.
Validate at build — jq empty fails fast before deploy.
Frequently Asked Questions
What is JSON?
JavaScript Object Notation — UTF-8 text using {} and [] with 6 value types (string, number, boolean, null, object, array), specified by RFC 8259 and json.org — used for APIs, config, and storage because it's human and machine readable.
Is JSON the same as a JavaScript object?
No: JSON is text ('{"a":1}' string), a JavaScript object is live memory ({a:1}). JSON.parse(text) → object, JSON.stringify(obj) → text.
Why must JSON keys be quoted?
Spec requires string keys ({"key":1}). Unquoted {key:1} is JavaScript object literal, not JSON — parsers throw Unexpected token k.
Can JSON have comments?
No — strictly per RFC 8259. Use JSON5 (// comment allowed) or JSONC for config comments, but interchange should be pure JSON.
How do I fix JSON trailing comma error?
Remove comma before } or ] → {"a":1, "b":2,} → {"a":1,"b":2}. Validate via formatter validator to highlight line:col.
When should I use JSON vs CSV?
JSON for nested APIs/config — preserves structure; CSV for flat tables for Excel. An array of flat JSON objects converts cleanly to CSV rows; nested JSON needs flattening.