JSON Cheat Sheet
This JSON cheat sheet covers 26 sections: the full syntax and every data type, parsing and serialising in JavaScript, Python, PHP, Java, C#, Go and Rust, jq, JSON Schema, JSON Lines and JSON5, databases, APIs, security and performance. Search it, filter it by level, copy any line with one click, and format or validate a document without leaving the page.
0 results
Nothing matches that query. Try a shorter keyword such as escape, stringify or jq select. Several words are combined, so every one of them has to match. Keys: / focuses the search, Esc clears it, t switches the theme.
Syntax & Data Types
the whole grammar, six value types, a playground
The Whole Grammar
Six values, two containers, one page of grammar
value = object | array | string | number | true | false | null
object = { } | { "key": value, "key": value, ... } keys are strings, always double quoted
array = [ ] | [ value, value, ... ] any mix of types, order matters
string = "..." double quotes only, with backslash escapes
number = -? 0 | [1-9][0-9]* ( . digits )? ( e|E [+-]? digits )?
literal = true | false | null lowercase, unquoted
whitespace = space, tab, line feed, carriage return, allowed between any two tokens
Every one of these is a complete, valid JSON document
{"name": "Ana"}
["a", "b", "c"]
"just a string"
42
-0.5e10
true
null
{}
[]
What JSON does not have
comments // no /* no */ # no
trailing commas [1, 2, 3,] {"a": 1,}
single quotes {'a': 'b'}
unquoted keys {a: 1}
undefined, NaN, Infinity, functions, Dates, regular expressions
hex, octal, leading +, leading zeros: 0x1F 017 +5 .5 5.
multi line strings without \n
duplicate key guarantees: "SHOULD be unique", most parsers keep the last one
Types at a Glance
| Type | Example | JavaScript | Python | Notes |
|---|---|---|---|---|
| object | {"a": 1} | Object | dict | unordered by spec, ordered in practice |
| array | [1, "a", null] | Array | list | ordered, heterogeneous |
| string | "caf\u00e9" | string | str | Unicode, double quotes, escapes |
| number | -12.5e3 | number (IEEE 754 double) | int or float | no int/float distinction in the grammar |
| boolean | true | boolean | True / False | lowercase in JSON |
| null | null | null | None | a value, not the absence of one |
Encoding and whitespace rules
encoding UTF-8 on the wire (RFC 8259 section 8.1); UTF-16 and UTF-32 only inside closed systems BOM must not be added; a parser may ignore one whitespace space (U+0020), tab (U+0009), LF (U+000A), CR (U+000D) only; no U+00A0, no form feed media type application/json, no charset parameter extension .json; JSON Lines .jsonl or .ndjson; JSON5 .json5; JSONC often still .json size no limit in the grammar; parsers and servers set their own
The Same Document in Four Formats
JSON: the interchange form
{
"name": "shop-api",
"version": "2.4.0",
"private": true,
"port": 8080,
"hosts": ["api.shop.example", "admin.shop.example"],
"database": {
"url": "postgres://shop@db:5432/shop",
"pool": 10
},
"description": "Line one.\nLine two."
}
JSON5: JSON for humans, needs a JSON5 parser
{
// comments are fine
name: 'shop-api',
version: '2.4.0',
private: true,
port: 0x1F90, // hex
hosts: [
'api.shop.example',
'admin.shop.example', // trailing comma is fine
],
database: { url: 'postgres://shop@db:5432/shop', pool: 10, },
description: 'Line one.\
Line two.',
}
YAML: indentation, comments, and type guessing
name: shop-api version: "2.4.0" # quote it, or some parsers read 2.4 as a float private: true port: 8080 hosts: - api.shop.example - admin.shop.example database: url: postgres://shop@db:5432/shop pool: 10 description: | Line one. Line two.
TOML: explicit types, sections, no nesting pain for flat config
name = "shop-api" version = "2.4.0" private = true port = 8080 hosts = ["api.shop.example", "admin.shop.example"] description = """ Line one. Line two.""" [database] url = "postgres://shop@db:5432/shop" pool = 10
Strings & Escapes
quotes, the nine escapes, Unicode, dates, binary
Escapes
The nine escapes, and the only ones allowed
\" double quote "She said \"hi\"" \\ backslash "C:\\Users\\ana" \/ forward slash "https:\/\/x.example" optional, "/" is fine raw \b backspace U+0008 \f form feed U+000C \n line feed "line one\nline two" \r carriage return \t tab \uXXXX any code unit, 4 hex digits "caf\u00e9" "\u00A9 2026" must be escaped: " \ and every control character U+0000 to U+001F may be raw: everything else, including é, 日本, emoji
Characters above U+FFFF need a surrogate pair
"\ud83d\ude00" grinning face, U+1F600 written as two \u escapes "😀" the same character, raw, preferred in UTF-8 "\u00e9" == "é" same string once parsed "e\u0301" e plus combining accent: looks the same, compares different (normalise with NFC)
Newlines, tabs and long text
"first line\nsecond line" the only way to write a line break
"col1\tcol2"
"a very long string cannot be split across lines in JSON; concatenate it before encoding"
multi line in supersets:
JSON5 'line one\
line two'
YAML description: |
line one
Things That Live in Strings
Dates and times: ISO 8601 strings, always with a zone
"2026-09-21T09:04:11Z" UTC, what JSON.stringify(new Date()) produces "2026-09-21T11:04:11+02:00" with an offset "2026-09-21T09:04:11.250Z" milliseconds "2026-09-21" a date only, no zone (a calendar day) "P3DT4H" an ISO 8601 duration avoid: "09/21/2026", "21.09.2026", 1758445451 (seconds or milliseconds?), "Mon Sep 21 2026"
Binary data: base64, or better, a URL
{"avatar": "iVBORw0KGgoAAAANSUhEUgAA...", "avatar_type": "image/png"} base64, +33% size
{"avatar_url": "https://cdn.example/u/12.png"} preferred for anything big
{"data": "data:image/png;base64,iVBORw0..."} a data URL, when the client wants one string
# URL safe base64 uses - and _ instead of + and /, and often drops the = padding (JWTs do this)
HTML, URLs and JSON inside JSON
{"html": "<p class=\"lead\">Hi</p>"} only the inner quotes need escaping
{"url": "https://x.example/a?b=1&c=2"} nothing to escape; \/ is optional
{"payload": "{\"nested\": true}"} JSON as a string: every quote escaped (double encoding, usually a mistake)
{"payload": {"nested": true}} what you probably meant
Keys are strings too
{"first name": "Ana"} legal; obj["first name"] in JavaScript, .["first name"] in jq
{"": "empty key"} legal
{"1": "a", "2": "b"} legal; JavaScript orders integer-like keys first when iterating
{"$ref": "#/defs/x"} legal; $ and @ carry meaning in JSON Schema, MongoDB and JSON-LD
{"a": 1, "a": 2} legal by grammar, undefined by spec; most parsers keep 2
Numbers, Booleans & null
precision limits, money, big ids, null versus absent
Numbers
Valid and invalid number forms
valid 0 -0 42 -17 3.14 0.5 1e10 1E-7 2.5e+3 1234567890123456789 invalid 01 +5 .5 5. 0x1F 1_000 NaN Infinity -Infinity 1e "42" (that is a string)
Precision: what a double can and cannot hold
safe integers -9007199254740991 to 9007199254740991 (2^53 - 1), Number.MAX_SAFE_INTEGER 64 bit ids 9223372036854775807 is valid JSON, but JavaScript rounds it: send "9223372036854775807" decimals 0.1 is not exact in binary; 0.1 + 0.2 != 0.3 exponent range about 5e-324 to 1.8e308; beyond that parsers give 0 or Infinity or an error Python int is unbounded, so 2**70 round trips; float is an IEEE 754 double like JavaScript Java, C#, Go choose the type: long, BigInteger, BigDecimal, decimal, json.Number
Money: integer minor units or a decimal string, never a float
{"amount": 1999, "currency": "EUR"} cents, what Stripe and most payment APIs do
{"amount": "19.99", "currency": "EUR"} a decimal string, parsed with Decimal / BigDecimal
{"amount": 19.99} wrong: 19.99 * 3 == 59.97000000000001
# a consistent alternative: a money object
{"price": {"units": 19, "nanos": 990000000, "currency_code": "EUR"}} Google's google.type.Money
Big integers without losing digits
// JavaScript: JSON.rawJSON and the reviver context (ES2024+, Chrome 114+, Node 21+)
JSON.stringify({id: JSON.rawJSON("9007199254740993")}) // '{"id":9007199254740993}'
JSON.parse(text, (key, value, ctx) => key === "id" ? BigInt(ctx.source) : value)
// BigInt has no default serialisation
JSON.stringify({n: 10n}) // TypeError
JSON.stringify({n: 10n}, (k, v) => typeof v === "bigint" ? v.toString() : v)
# Python: exact by default
json.loads("9007199254740993") # 9007199254740993, an int
json.loads("1.10", parse_float=Decimal) # Decimal('1.10')
Booleans and null
Lowercase, unquoted, and not numbers
valid true false null invalid True FALSE Null None nil "true" 1 (a number, not a boolean) undefined
null versus a missing key
{"middle_name": null} present, explicitly empty; a PATCH with this clears the field
{} absent; a PATCH with this leaves middle_name alone
{"middle_name": ""} present, an empty string, which is a third state
JavaScript obj.middle_name === null vs !("middle_name" in obj) vs obj.middle_name === undefined (both cases)
Python d["middle_name"] is None vs "middle_name" not in d
jq .middle_name == null is true for BOTH null and absent; use has("middle_name")
Truthiness is a language thing, not a JSON thing
JSON value JavaScript if() Python if jq if
0 false False true (jq: only false and null are falsy)
"" false False true
[] true False true
{} true False true
null false False false
"false" true True true (a non empty string)
Objects & Arrays
nesting, ordering, shapes that work and shapes that hurt
Rules
Objects are unordered by spec, arrays are ordered
{"b": 1, "a": 2} == {"a": 2, "b": 1} the same object; do not rely on order
[1, 2] != [2, 1] different arrays
JavaScript quirk: JSON.parse('{"b":1,"2":"x","a":2,"1":"y"}') iterates as 1, 2, b, a
Go maps: encoding/json sorts keys alphabetically on output
Python: dicts keep insertion order; json.dumps(sort_keys=True) to sort
Nesting is unlimited, in theory
{"a": {"b": {"c": {"d": [[[["deep"]]]]}}}} valid
limits parsers apply:
Python json ~1000 levels (RecursionError)
.NET MaxDepth 64 by default
Jackson 1000 by default (StreamReadConstraints)
PHP 512 by default, the depth argument
browsers tens of thousands, then a stack overflow
# deep nesting is a denial of service vector; see Security
Arrays: any mix, any depth, empty is fine
[1, "two", true, null, {"five": 5}, [6]] legal, rarely wise
[[1, 2, 3], [4, 5, 6]] a matrix
[{"id": 1}, {"id": 2}] the usual list of records
[] empty, not null; prefer it over null for "no items"
Shapes That Work
A list of records versus a map keyed by id
// list of records: ordered, easy to page, easy to add fields, what most APIs return
{"users": [
{"id": 12, "name": "Ana", "role": "admin"},
{"id": 15, "name": "Dan", "role": "editor"}
]}
// map keyed by id: O(1) lookup, unique keys, order not guaranteed, keys are strings
{"users": {
"12": {"name": "Ana", "role": "admin"},
"15": {"name": "Dan", "role": "editor"}
}}
Shapes that hurt, and their fixes
// parallel arrays: positions must stay aligned by hand
{"ids": [12, 15], "names": ["Ana", "Dan"]}
// fix
[{"id": 12, "name": "Ana"}, {"id": 15, "name": "Dan"}]
// positional tuples: what is index 2?
["Ana", 34, true]
// fix
{"name": "Ana", "age": 34, "active": true}
// keys that carry data: impossible to validate or type
{"2026-09-21": 12, "2026-09-22": 15}
// fix
[{"date": "2026-09-21", "count": 12}, {"date": "2026-09-22", "count": 15}]
// stringly typed booleans and numbers
{"active": "true", "count": "3"}
// fix
{"active": true, "count": 3}
Envelopes, pagination, and polymorphism
// a collection response with room for metadata
{"data": [...], "meta": {"total": 128, "page": 2, "per_page": 25}, "links": {"next": "/users?page=3"}}
// cursor pagination
{"data": [...], "next_cursor": "eyJpZCI6MTV9", "has_more": true}
// polymorphic items: a discriminator field, never "guess by which keys exist"
{"events": [
{"type": "click", "x": 10, "y": 20},
{"type": "key", "code": "Enter"}
]}
// a single value at the top level is legal but leaves no room to grow
[{"id": 1}] vs {"data": [{"id": 1}]}
Walking and Reshaping
Visit every leaf with its path
function walk(value, path = "", out = []) {
if (Array.isArray(value)) value.forEach((v, i) => walk(v, `${path}[${i}]`, out));
else if (value && typeof value === "object") Object.entries(value).forEach(([k, v]) => walk(v, path ? `${path}.${k}` : k, out));
else out.push([path, value]);
return out;
}
walk(order).filter(([p]) => /sku|name|qty/.test(p)).forEach(([p, v]) => console.log(`${p} = ${JSON.stringify(v)}`));
# Python
def walk(v, path=""):
if isinstance(v, dict): [yield from walk(x, f"{path}.{k}" if path else k) for k, x in v.items()]
elif isinstance(v, list): [yield from walk(x, f"{path}[{i}]") for i, x in enumerate(v)]
else: yield path, v
Flatten and unflatten
const flatten = (obj, prefix = "", out = {}) => {
for (const [k, v] of Object.entries(obj)) {
const key = prefix ? `${prefix}.${k}` : k;
v && typeof v === "object" ? flatten(v, key, out) : (out[key] = v);
}
return out;
};
const unflatten = (flat) => Object.entries(flat).reduce((acc, [path, v]) => {
path.split(".").reduce((o, k, i, a) => o[k] ??= i === a.length - 1 ? v : (/^\d+$/.test(a[i + 1]) ? [] : {}), acc);
return acc;
}, {});
npm i flat flatten(obj) / unflatten(obj) pip install flatten-json flatten(d, ".")
jq: [paths(scalars) as $p | {key: ($p | map(tostring) | join(".")), value: getpath($p)}] | from_entries
Compare, merge, pick and omit
// deep equality: structural, key order ignored
const deepEqual = (a, b) => a === b || (a && b && typeof a === "object" && typeof b === "object" &&
Array.isArray(a) === Array.isArray(b) && Object.keys(a).length === Object.keys(b).length &&
Object.keys(a).every((k) => deepEqual(a[k], b[k])));
import {isEqual, merge, pick, omit, get, set} from "lodash-es"; // or node:util isDeepStrictEqual
get(order, "items[0].sku", "none"); set(order, "customer.vip", true); pick(order, ["id", "status"]); omit(order, ["coupon"]);
const merged = {...defaults, ...override}; // shallow: nested objects are replaced, not merged
const deep = merge({}, defaults, override); // recursive, arrays merged by index (surprising)
# Python
a == b # dicts and lists compare structurally, key order ignored
{**defaults, **override} # shallow merge; defaults | override in 3.9+
from deepmerge import always_merger # recursive
{k: d[k] for k in ("id", "status") if k in d}
Valid vs Invalid JSON
every classic mistake next to its fix
Side by Side
| Invalid | Why | Valid |
|---|---|---|
{'name': 'Ana'} | single quotes | {"name": "Ana"} |
{name: "Ana"} | unquoted key | {"name": "Ana"} |
[1, 2, 3,] | trailing comma | [1, 2, 3] |
{"a": 1,} | trailing comma | {"a": 1} |
{"a": 1 "b": 2} | missing comma | {"a": 1, "b": 2} |
{"a": 1} // note | comment | {"a": 1, "_note": "..."} |
{"ok": True} | capitalised literal | {"ok": true} |
{"v": undefined} | not a JSON value | {"v": null} or omit the key |
{"n": NaN} | not a JSON value | {"n": null} or {"n": "NaN"} |
{"n": 007} | leading zero | {"n": 7} or {"n": "007"} |
{"n": .5} | no leading digit | {"n": 0.5} |
{"n": +1} | leading plus | {"n": 1} |
{"p": "C:\Users"} | \U is not an escape | {"p": "C:\\Users"} |
{"s": "line | raw newline in a string | {"s": "line\nbreak"} |
{"s": "tab here"} | raw tab in a string | {"s": "tab\there"} |
{"d": new Date()} | JavaScript, not JSON | {"d": "2026-09-21T09:04:11Z"} |
{"a": 1}{"b": 2} | two documents | [{"a": 1}, {"b": 2}] or JSON Lines |
"unterminated | missing closing quote | "terminated" |
{"a": [1, 2} | bracket mismatch | {"a": [1, 2]} |
| (empty file) | no value at all | null, {} or [] |
{"a": 1} | byte order mark | save as UTF-8 without BOM |
Find the Error Fast
Three validators that print the position
jq empty file.json # exit code 0 when valid, message with line and column when not
python -m json.tool file.json # same, and pretty prints when valid
node -e 'JSON.parse(require("fs").readFileSync(0, "utf8"))' < file.json
# or paste into the playground at the top of this page
Reading the message
"Unexpected token } in JSON at position 12" a trailing comma just before the } "Expected ',' or '}' after property value" missing comma, or a raw newline inside a string "Unexpected end of JSON input" truncated file, unbalanced brackets, or an empty body "Unexpected token u in JSON at position 0" you parsed the string "undefined": the variable was never set "Unexpected token < in JSON at position 0" the server returned an HTML error page, not JSON "Bad control character in string literal" a raw tab or newline inside quotes
When you really want comments
// JSONC: comments allowed, used by VS Code settings.json and tsconfig.json
{
// compiler options
"compilerOptions": { "strict": true }
}
// plain JSON: a convention the consumer ignores
{"_comment": "rotated weekly", "keys": ["a", "b"]}
// or strip comments before parsing
npm i strip-json-comments JSON.parse(stripJsonComments(text))
python: json5 or commentjson or move the file to YAML / TOML
Repair, Windows, and the Edges
Fix almost valid JSON automatically
npm i jsonrepair JSON.parse(jsonrepair(text)) // also a CLI: jsonrepair broken.json
pip install json-repair json_repair.loads(text) # or repair_json(text) for the string
npm i json5 JSON5.parse(text) // accepts comments, trailing commas, single quotes
npm i dirty-json dJSON.parse(text) // very tolerant, older
python: ast.literal_eval(text) # when the "JSON" is really a Python repr: {'a': True, 'b': None}
jq -n 'input' 2>&1 | head -1 # says which line breaks first
Windows: PowerShell, cmd quoting, line endings
$data = Get-Content order.json -Raw | ConvertFrom-Json # objects with properties: $data.customer.name
$data | ConvertTo-Json -Depth 10 | Set-Content out.json # -Depth! the default 2 turns deeper levels into text
Invoke-RestMethod -Uri https://api.example/products -Method Post -ContentType "application/json" -Body ($body | ConvertTo-Json)
[System.IO.File]::WriteAllText("out.json", $json, (New-Object System.Text.UTF8Encoding $false)) # UTF-8 without BOM
:: cmd.exe: no single quotes, escape the inner double quotes
curl -X POST https://api.example/products -H "Content-Type: application/json" -d "{\"name\": \"Desk\"}"
:: or keep the body in a file
curl -X POST https://api.example/products -H "Content-Type: application/json" -d @body.json
CRLF line endings fine: \r\n is whitespace between tokens; inside a string it is an error like any raw newline
trailing garbage '{"a": 1}}' or '{"a": 1} ok' fails: "Unexpected non-whitespace character after JSON"
Valid but surprising
{"a": 1, "a": 2} valid, last wins in most parsers, first in a few, error in strict ones
1e400 valid syntax; a double overflows to Infinity, Python raises nothing and gives inf
-0 valid; JavaScript keeps -0, JSON.stringify(-0) gives "0"
" " valid; a NUL inside a string breaks C strings and some databases (PostgreSQL rejects it in jsonb)
"\ud800" valid syntax, a lone surrogate: not valid Unicode, rejected by strict parsers, replaced with U+FFFD by others
[] valid; so is "" and 0; an empty file is not
{"a": 1} leading whitespace, including a newline, is fine; a BOM is not
JavaScript
parse, stringify, reviver, replacer, fetch, files
JSON.parse and JSON.stringify
Text to value, value to text
const obj = JSON.parse('{"id": 12, "tags": ["a", "b"], "active": true}');
const text = JSON.stringify(obj); // compact, one line
const pretty = JSON.stringify(obj, null, 2); // two space indent
JSON.stringify(obj, null, "\t"); // or any string up to 10 characters
What stringify drops or changes
JSON.stringify({
a: 1,
b: undefined, // dropped
c: () => 1, // dropped
d: NaN, // null
e: new Date(1789981451000), // "2026-09-21T09:04:11.000Z" via toJSON
f: Symbol("x"), // dropped
g: new Map([["k", 1]]), // {} use Object.fromEntries(map) or [...map]
h: [undefined, () => 1, NaN], // [null, null, null]
});
Circular references throw; break the cycle in a replacer
const a = {name: "a"}; a.self = a;
JSON.stringify(a); // TypeError
const seen = new WeakSet();
JSON.stringify(a, (key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) { return "[Circular]"; }
seen.add(value);
}
return value;
});
Reviver and replacer: transform on the way in and out
// reviver runs bottom up on every key; return undefined to delete the key
JSON.parse(text, (key, value) =>
typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value) ? new Date(value) : value);
// replacer as a function
JSON.stringify(obj, (key, value) => key.startsWith("_") ? undefined : value);
// replacer as an allow list of keys (applies at every depth)
JSON.stringify(user, ["id", "name", "email"]);
Everyday Patterns
fetch: send and receive JSON
const res = await fetch("/api/products", {
method: "POST",
headers: {"Content-Type": "application/json", "Accept": "application/json"},
body: JSON.stringify({name: "Desk", price: 199}),
});
if (!res.ok) { throw new Error(`HTTP ${res.status}: ${await res.text()}`); }
const product = await res.json();
// GET with a query string built safely
const url = new URL("/api/products", location.origin);
url.searchParams.set("q", "oak & walnut");
const list = await (await fetch(url)).json();
Custom serialisation with toJSON
class Money {
constructor(cents, currency) { this.cents = cents; this.currency = currency; }
toJSON() { return {amount: (this.cents / 100).toFixed(2), currency: this.currency}; }
}
JSON.stringify({price: new Money(19999, "EUR")}); // {"price":{"amount":"199.99","currency":"EUR"}}
// Date.prototype.toJSON is why dates become ISO strings
// there is no fromJSON; use a reviver or a constructor that accepts the plain object
Deep clone: structuredClone, not parse(stringify())
const copy = structuredClone(original); // keeps Date, Map, Set, cycles; drops functions const copy2 = JSON.parse(JSON.stringify(original)); // plain data only, Dates become strings
Storage and files in the browser
localStorage.setItem("cart", JSON.stringify(cart));
const cart = JSON.parse(localStorage.getItem("cart") ?? "[]"); // getItem returns null when missing
// download a JSON file
const blob = new Blob([JSON.stringify(data, null, 2)], {type: "application/json"});
const a = Object.assign(document.createElement("a"), {href: URL.createObjectURL(blob), download: "export.json"});
a.click(); URL.revokeObjectURL(a.href);
// read an uploaded file
const data = JSON.parse(await fileInput.files[0].text());
Node: files and imports
import {readFile, writeFile} from "node:fs/promises";
const config = JSON.parse(await readFile("config.json", "utf8"));
await writeFile("out.json", JSON.stringify(data, null, 2) + "\n");
// import a JSON module (ESM, Node 20.10+ and browsers with import attributes)
import pkg from "./package.json" with {type: "json"};
const pkg = require("./package.json"); // CommonJS, cached after the first read
// Node 21+ can parse from a stream without buffering the whole body
const body = await new Response(req).json();
Edge Cases and Newer APIs
Details worth knowing
JSON.parse('{"__proto__": 1}') // safe: an own property named __proto__, no prototype pollution
Object.assign({}, JSON.parse(...)) // spreading it later CAN pollute; see Security
JSON.stringify(undefined) // undefined (not a string!)
JSON.stringify(null) // "null"
JSON.parse("") // SyntaxError; guard empty bodies
JSON.parse(" 1 ") // 1, surrounding whitespace is fine
JSON.stringify("é") // '"é"'; raw UTF-8, only control chars and quotes are escaped
JSON.stringify({a: 1}, null, 2) === JSON.stringify({a: 1}, null, 2) // stable for equal input, key order = insertion
JSON.rawJSON and JSON.isRawJSON (ES2024, Chrome 114+, Node 21+)
// emit a number the runtime cannot represent, without quoting it
JSON.stringify({id: JSON.rawJSON("12345678901234567890"), amount: JSON.rawJSON("19.990")});
// '{"id":12345678901234567890,"amount":19.990}'
JSON.isRawJSON(JSON.rawJSON("1")) // true
// the reviver receives the source text of primitives
JSON.parse('{"id": 12345678901234567890}', (key, value, {source}) => key === "id" ? BigInt(source) : value);
Deterministic output for hashing and diffs
const canon = (v) => JSON.stringify(v, (key, value) =>
value && typeof value === "object" && !Array.isArray(value)
? Object.fromEntries(Object.keys(value).sort().map((k) => [k, value[k]]))
: value);
canon({b: 1, a: {d: 1, c: 2}}); // '{"a":{"c":2,"d":1},"b":1}'
npm i canonicalize // RFC 8785 JSON Canonicalization Scheme, exact number formatting too
npm i fast-json-stable-stringify // sorted keys, widely used
HTTP Clients and Storage
axios versus fetch
import axios from "axios";
const {data} = await axios.post("/api/products", {name: "Desk", price: 199}); // object body: JSON automatically
const list = (await axios.get("/api/products", {params: {q: "oak"}})).data; // parsed already
try { await axios.get("/api/missing"); } catch (e) { e.response?.status; e.response?.data; } // rejects on 4xx/5xx
// fetch: nothing automatic
const res = await fetch("/api/products", {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(body)});
if (!res.ok) throw new Error(String(res.status)); // fetch resolves on 404
const data = await res.json(); // throws on a non JSON body
// XMLHttpRequest, when you meet it
const xhr = new XMLHttpRequest(); xhr.open("GET", "/api/products"); xhr.responseType = "json";
xhr.onload = () => console.log(xhr.response); // parsed, or null when the body is not JSON
xhr.send();
Response.json() and Request bodies without stringify
return Response.json({ok: true}, {status: 201}); // sets Content-Type: application/json; service workers, Cloudflare, Deno, Bun, Node 18+
export default { fetch: async (req) => Response.json(await req.json()) }; // edge runtimes: parse in, JSON out
const body = await new Response(stream).json(); // parse any ReadableStream as JSON
new Blob([JSON.stringify(data)], {type: "application/json"}).text();
Storage: strings in localStorage, structured values in IndexedDB
// localStorage / sessionStorage: strings only
try { localStorage.setItem("prefs", JSON.stringify(prefs)); } catch (e) { /* QuotaExceededError, private mode */ }
const prefs = JSON.parse(localStorage.getItem("prefs") ?? "{}");
window.addEventListener("storage", (e) => { if (e.key === "prefs") apply(JSON.parse(e.newValue)); }); // other tabs
// IndexedDB: no JSON needed, values are cloned
import {openDB} from "idb";
const db = await openDB("shop", 1, {upgrade(db) { db.createObjectStore("orders", {keyPath: "id"}); }});
await db.put("orders", {id: 12, placed: new Date(), items: [...]}); // Date survives
const order = await db.get("orders", 12);
// postMessage / Worker: structured clone, no stringify
worker.postMessage({type: "parse", payload: bigObject});
// cookies: strings, 4 KB, and never raw JSON with ; or , in it: encodeURIComponent(JSON.stringify(v))
TypeScript
typing parsed JSON, runtime validation, JSON modules
Types for JSON
A type for any JSON value, and why parse returns any
type JsonPrimitive = string | number | boolean | null;
type JsonValue = JsonPrimitive | JsonValue[] | {[key: string]: JsonValue};
type JsonObject = {[key: string]: JsonValue};
const raw = JSON.parse(text); // any: no checking from here on
const data: unknown = JSON.parse(text); // better: you must narrow before use
const product = JSON.parse(text) as Product; // a lie the compiler believes
A hand written type guard
interface Product { id: number; name: string; tags: string[]; price?: number }
function isProduct(v: unknown): v is Product {
return typeof v === "object" && v !== null
&& typeof (v as any).id === "number"
&& typeof (v as any).name === "string"
&& Array.isArray((v as any).tags) && (v as any).tags.every((t: unknown) => typeof t === "string");
}
const data: unknown = await res.json();
if (!isProduct(data)) { throw new Error("bad payload"); }
data.name; // typed
Zod: one schema, both the check and the type
import {z} from "zod";
const Product = z.object({
id: z.number().int().positive(),
name: z.string().min(1),
tags: z.array(z.string()).default([]),
price: z.number().nonnegative().optional(),
createdAt: z.string().datetime().transform((s) => new Date(s)),
});
type Product = z.infer<typeof Product>;
const product = Product.parse(await res.json()); // throws ZodError with paths
const result = Product.safeParse(JSON.parse(text)); // {success, data} or {success, error}
const list = z.array(Product).parse(data);
Modules, Literals, Config
Import a JSON file with types
{
"compilerOptions": {
"resolveJsonModule": true,
"esModuleInterop": true,
"module": "NodeNext", // then use import attributes
}
}
import config from "./config.json" with {type: "json"};
config.port; // typed from the file's literal shape: number
as const and satisfies for JSON-like literals
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
const settings = {
theme: "dark",
retries: 3,
} satisfies Record<string, JsonValue>; // checked against the type, keeps the literal types
// a typed response helper
async function getJson<T>(url: string, guard: (v: unknown) => v is T): Promise<T> {
const data: unknown = await (await fetch(url)).json();
if (!guard(data)) { throw new TypeError(`Unexpected payload from ${url}`); }
return data;
}
Generate types from JSON and from a schema
npx quicktype sample.json -o Product.ts --lang ts # types from an example document npx json-schema-to-typescript schema.json > types.d.ts # types from a JSON Schema npx openapi-typescript openapi.json -o api.d.ts # types for a whole API npx ts-json-schema-generator --path types.ts --type Product # the other direction
Python
the json module, dataclasses, pydantic, orjson
The json Module
loads and dumps for strings, load and dump for files
import json
data = json.loads('{"id": 12, "tags": ["a", "b"], "active": true, "nothing": null}')
text = json.dumps(data)
with open("config.json", encoding="utf-8") as f:
config = json.load(f)
with open("out.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
f.write("\n")
from pathlib import Path
config = json.loads(Path("config.json").read_text(encoding="utf-8"))
dumps options you will use
json.dumps(data, indent=2) # pretty
json.dumps(data, separators=(",", ":")) # compact, no spaces
json.dumps(data, sort_keys=True) # deterministic
json.dumps(data, ensure_ascii=False) # keep "café" as is instead of "caf\u00e9"
json.dumps(data, default=str) # anything unknown becomes str(value): dates, Decimal, UUID
json.dumps(float("nan")) # 'NaN': valid Python, INVALID JSON; allow_nan=False raises instead
Type mapping, both directions
JSON Python (loads) Python (dumps) accepts object dict dict array list list, tuple string str str number int or float int, float, (bool is int, so True becomes true) true/false True/False bool null None None # not serialisable by default: datetime, date, Decimal, UUID, set, bytes, dataclass, Enum
Errors, and parsing numbers exactly
try:
data = json.loads(text)
except json.JSONDecodeError as e:
print(e.msg, e.lineno, e.colno, e.pos)
from decimal import Decimal
json.loads('{"price": 19.99}', parse_float=Decimal) # Decimal('19.99'), no float rounding
json.loads('{"n": 1}', parse_int=str) # keep huge ints as text
json.loads(text, object_pairs_hook=OrderedDict) # or detect duplicate keys with a custom hook
Custom Types
A default function or an encoder subclass
from datetime import date, datetime
from decimal import Decimal
from uuid import UUID
def to_json(o):
if isinstance(o, (datetime, date)): return o.isoformat()
if isinstance(o, Decimal): return str(o)
if isinstance(o, UUID): return str(o)
if isinstance(o, set): return sorted(o)
raise TypeError(f"not serialisable: {type(o).__name__}")
json.dumps(record, default=to_json)
class AppEncoder(json.JSONEncoder):
def default(self, o): return to_json(o) # falls back to super().default for the error
json.dumps(record, cls=AppEncoder)
Dataclasses in and out
from dataclasses import dataclass, asdict, field
@dataclass
class Product:
id: int
name: str
tags: list[str] = field(default_factory=list)
json.dumps(asdict(Product(1, "Desk"))) # {"id": 1, "name": "Desk", "tags": []}
Product(**json.loads(text)) # no validation: wrong types pass silently
pydantic: validate, convert, and produce a schema
from pydantic import BaseModel, Field, HttpUrl
from datetime import datetime
class Product(BaseModel):
id: int
name: str = Field(min_length=1)
price: float = Field(ge=0)
url: HttpUrl | None = None
created_at: datetime
p = Product.model_validate_json(text) # parse + validate in one step
p.model_dump_json(indent=2) # back to JSON, dates as ISO strings
p.model_dump(mode="json") # a JSON safe dict
Product.model_json_schema() # a JSON Schema for the model
Faster: orjson
pip install orjson
import orjson
orjson.loads(b'{"a": 1}') # accepts bytes or str
orjson.dumps(data) # returns bytes
orjson.dumps(data, option=orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS | orjson.OPT_NAIVE_UTC)
orjson.dumps(record, default=to_json) # same default hook idea
# datetime, date, dataclass, UUID, numpy arrays: handled without a default
HTTP with requests and httpx
import httpx
r = httpx.post("https://api.example/products", json={"name": "Desk"}, headers={"Accept": "application/json"})
r.raise_for_status()
product = r.json() # raises json.JSONDecodeError on a non JSON body
import requests
r = requests.get("https://api.example/products", params={"q": "oak"}, timeout=10)
items = r.json()["data"]
PHP
json_encode, json_decode, flags, JsonSerializable
Encode and Decode
Objects or associative arrays, your choice
$obj = json_decode('{"id": 12, "tags": ["a", "b"]}'); // stdClass: $obj->id
$arr = json_decode('{"id": 12, "tags": ["a", "b"]}', true); // array: $arr['id']
echo json_encode(['id' => 12, 'tags' => ['a', 'b']]);
echo json_encode($obj, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
Arrays become lists or objects depending on their keys
json_encode([1, 2, 3]); // [1,2,3]
json_encode(['a' => 1]); // {"a":1}
json_encode([]); // [] (an empty array is a list)
json_encode([], JSON_FORCE_OBJECT); // {}
json_encode([1 => 'a', 2 => 'b']); // {"1":"a","2":"b"} keys do not start at 0
json_encode(array_values($list)); // re-index to get a list again
json_encode(new stdClass); // {}
The flags that matter
JSON_PRETTY_PRINT four space indent JSON_UNESCAPED_UNICODE "café" instead of "caf\u00e9" JSON_UNESCAPED_SLASHES "https://x" instead of "https:\/\/x" JSON_THROW_ON_ERROR throw JsonException instead of returning null / false (PHP 7.3+) JSON_PRESERVE_ZERO_FRACTION 10.0 stays 10.0 instead of 10 JSON_NUMERIC_CHECK "12" becomes 12 (dangerous on phone numbers and zip codes) JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT safe for embedding in HTML JSON_BIGINT_AS_STRING decode: keep big ints as strings instead of floats JSON_INVALID_UTF8_SUBSTITUTE replace bad bytes with U+FFFD instead of failing JSON_PARTIAL_OUTPUT_ON_ERROR encode what you can, null for the rest
Errors: throw, do not check for null
try {
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
http_response_code(400);
echo json_encode(['error' => 'Invalid JSON: ' . $e->getMessage()]);
}
// the old way
$data = json_decode($body, true);
if (json_last_error() !== JSON_ERROR_NONE) { echo json_last_error_msg(), ' (', json_last_error(), ')'; }
json_validate($body); // PHP 8.3+: true/false without building the value
Classes and Frameworks
JsonSerializable controls what an object becomes
final class Money implements JsonSerializable
{
public function __construct(private int $cents, private string $currency) {}
public function jsonSerialize(): array
{
return ['amount' => number_format($this->cents / 100, 2, '.', ''), 'currency' => $this->currency];
}
}
echo json_encode(['price' => new Money(19999, 'EUR')]); // {"price":{"amount":"199.99","currency":"EUR"}}
// without the interface: public properties only, private ones are skipped
// enums: backed enums encode as their value, pure enums throw
Read a request body, send a response
// plain PHP
$data = json_decode(file_get_contents('php://input'), true, 512, JSON_THROW_ON_ERROR);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => true], JSON_THROW_ON_ERROR);
// Laravel
$data = $request->json()->all(); $name = $request->input('name'); $validated = $request->validate([...]);
return response()->json(['data' => $products], 200, [], JSON_UNESCAPED_UNICODE);
return ProductResource::collection($products); // API resources shape the JSON
// WordPress
wp_send_json_success(['id' => 12]); wp_send_json_error('Bad input', 400); wp_json_encode($data);
register_rest_route('shop/v1', '/products', ['methods' => 'GET', 'callback' => fn() => ['data' => []]]);
Big numbers, depth and typed mapping
json_decode('{"id": 9007199254740993}', true, 512, JSON_BIGINT_AS_STRING); // "9007199254740993"
json_decode($deep, true, 8); // depth limit, "Maximum stack depth exceeded" beyond it
json_decode($text, false, 512, JSON_OBJECT_AS_ARRAY);
// map JSON to typed objects
composer require symfony/serializer $serializer->deserialize($json, Product::class, 'json');
composer require cuyz/valinor (new MapperBuilder())->mapper()->map(Product::class, Source::json($json));
// PHP 8.4 has no built in typed decoder; ext-simdjson and ext-json remain the fast paths
Java & Kotlin
Jackson, Gson, kotlinx.serialization, Spring
Jackson
A record in and out with one ObjectMapper
// build.gradle: implementation("com.fasterxml.jackson.core:jackson-databind:2.18.0")
import com.fasterxml.jackson.databind.ObjectMapper;
public record Product(long id, String name, List<String> tags, double price) {}
static final ObjectMapper MAPPER = new ObjectMapper()
.findAndRegisterModules(); // JavaTimeModule for java.time, Jdk8Module for Optional
Product p = MAPPER.readValue(json, Product.class);
String out = MAPPER.writeValueAsString(p);
String pretty = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(p);
List<Product> list = MAPPER.readValue(json, new TypeReference<List<Product>>() {});
Annotations you reach for
@JsonProperty("created_at") Instant createdAt; // rename one field
@JsonIgnore String passwordHash; // never serialise
@JsonInclude(JsonInclude.Include.NON_NULL) // on a class: skip nulls
@JsonIgnoreProperties(ignoreUnknown = true) // tolerate extra keys
@JsonFormat(shape = STRING, pattern = "yyyy-MM-dd") // dates as strings
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) // camelCase <-> snake_case for the whole class
@JsonTypeInfo(use = NAME, property = "type") @JsonSubTypes({...}) // polymorphism with a discriminator
@JsonCreator + @JsonProperty on constructor params // immutable classes without a default constructor
The mapper settings everyone changes
MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // "2026-09-21T09:04:11Z", not 1789981451 MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL); MAPPER.enable(SerializationFeature.INDENT_OUTPUT); MAPPER.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); MAPPER.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); // money safe JsonMapper.builder().enable(...).build(); // the 2.x builder style
Tree model and streaming, when you do not have a class
JsonNode root = MAPPER.readTree(json);
String name = root.path("customer").path("name").asText("unknown"); // path() never throws, get() returns null
for (JsonNode item : root.withArray("items")) { item.get("sku").asText(); }
ObjectNode obj = MAPPER.createObjectNode().put("ok", true);
obj.putArray("tags").add("a").add("b");
// streaming: constant memory over a huge array
try (JsonParser parser = MAPPER.createParser(new File("big.json"))) {
while (parser.nextToken() != null) {
if (parser.currentToken() == JsonToken.START_OBJECT) { Product p = MAPPER.readValue(parser, Product.class); }
}
}
Gson, Kotlin, Spring
Gson: smaller, reflection based, common on Android
Gson gson = new GsonBuilder().setPrettyPrinting().serializeNulls().create();
Product p = gson.fromJson(json, Product.class);
String out = gson.toJson(p);
List<Product> list = gson.fromJson(json, new TypeToken<List<Product>>() {}.getType());
@SerializedName("created_at") String createdAt;
// Gson ignores unknown fields by default and has no java.time support without an adapter
Kotlin: kotlinx.serialization
// plugin: kotlin("plugin.serialization"); dependency: kotlinx-serialization-json
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class Product(val id: Long, val name: String, val tags: List<String> = emptyList(), @SerialName("created_at") val createdAt: String? = null)
val json = Json { ignoreUnknownKeys = true; prettyPrint = false; explicitNulls = false; encodeDefaults = true }
val p = json.decodeFromString<Product>(text)
val out = json.encodeToString(p)
val list = json.decodeFromString<List<Product>>(text)
// dynamic
val el = Json.parseToJsonElement(text)
el.jsonObject["customer"]?.jsonObject?.get("name")?.jsonPrimitive?.content
buildJsonObject { put("ok", true); putJsonArray("tags") { add("a") } }
Spring Boot: JSON in and out of a controller
@RestController
@RequestMapping("/api/products")
class ProductController {
@PostMapping
ResponseEntity<Product> create(@Valid @RequestBody CreateProduct body) { // Jackson + Bean Validation
Product saved = service.create(body);
return ResponseEntity.status(HttpStatus.CREATED).body(saved); // serialised by Jackson
}
@GetMapping("/{id}")
Product get(@PathVariable long id) { return service.get(id); } // 200 with application/json
}
// application.yml
spring.jackson.property-naming-strategy: SNAKE_CASE
spring.jackson.default-property-inclusion: non_null
spring.jackson.deserialization.fail-on-unknown-properties: false
Jakarta JSON-B and JSON-P, the standard APIs
Jsonb jsonb = JsonbBuilder.create(new JsonbConfig().withFormatting(true));
Product p = jsonb.fromJson(json, Product.class); String out = jsonb.toJson(p);
@JsonbProperty("created_at") @JsonbTransient @JsonbDateFormat("yyyy-MM-dd")
JsonObject obj = Json.createReader(new StringReader(json)).readObject(); // JSON-P tree
obj.getString("name"); obj.getJsonArray("tags").getString(0);
Json.createObjectBuilder().add("ok", true).build().toString();
C# & .NET
System.Text.Json, Json.NET, ASP.NET Core
System.Text.Json
Serialize and deserialize with shared options
using System.Text.Json;
using System.Text.Json.Serialization;
public record Product(long Id, string Name, List<string> Tags, DateTimeOffset CreatedAt);
static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web) // camelCase, case insensitive
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
Converters = { new JsonStringEnumConverter() },
};
var p = JsonSerializer.Deserialize<Product>(json, Options)!;
string text = JsonSerializer.Serialize(p, Options);
var list = JsonSerializer.Deserialize<List<Product>>(json, Options);
await JsonSerializer.SerializeAsync(stream, p, Options); // straight to a stream
Attributes
[JsonPropertyName("created_at")] public DateTimeOffset CreatedAt { get; init; }
[JsonIgnore] public string PasswordHash { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
[JsonPropertyOrder(1)]
[JsonNumberHandling(JsonNumberHandling.WriteAsString)] public long Id { get; init; } // big ids safe for JavaScript
[JsonConverter(typeof(MoneyConverter))]
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] [JsonDerivedType(typeof(Click), "click")] // .NET 7+
[JsonExtensionData] public Dictionary<string, JsonElement>? Extra { get; set; } // keep unknown keys
Without a class: JsonDocument, JsonNode, dynamic edits
using var doc = JsonDocument.Parse(json); // read only, pooled, dispose it
JsonElement root = doc.RootElement;
Console.WriteLine(root.GetProperty("name").GetString());
if (root.TryGetProperty("price", out var price)) Console.WriteLine(price.GetDouble());
foreach (var item in root.GetProperty("items").EnumerateArray()) { item.GetProperty("sku").GetString(); }
JsonNode node = JsonNode.Parse(json)!; // mutable tree
node["paid"] = true; node["tags"]!.AsArray().Add("new");
Console.WriteLine(node["paid"]!.GetValue<bool>());
node.ToJsonString(new JsonSerializerOptions { WriteIndented = true });
Source generation and the low level reader
[JsonSerializable(typeof(Product))]
[JsonSerializable(typeof(List<Product>))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
internal partial class AppJsonContext : JsonSerializerContext { }
var p = JsonSerializer.Deserialize(json, AppJsonContext.Default.Product);
JsonSerializer.Serialize(p, AppJsonContext.Default.Product);
var reader = new Utf8JsonReader(bytes);
while (reader.Read())
if (reader.TokenType == JsonTokenType.PropertyName && reader.ValueTextEquals("sku")) { reader.Read(); var sku = reader.GetString(); }
ASP.NET Core and Json.NET
Minimal API and controllers: JSON is the default
app.MapPost("/api/products", (CreateProduct body, IProductService svc) =>
Results.Created($"/api/products/{svc.Create(body).Id}", body)); // body bound from JSON, response is JSON
app.MapGet("/api/products/{id:long}", (long id) => Results.Ok(svc.Get(id)));
builder.Services.ConfigureHttpJsonOptions(o => { // minimal APIs
o.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
o.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
builder.Services.AddControllers().AddJsonOptions(o => { ... }); // controllers
[ApiController] public class ProductsController : ControllerBase {
[HttpPost] public ActionResult<Product> Create([FromBody] CreateProduct body) => CreatedAtAction(...);
}
HttpClient helpers
using System.Net.Http.Json;
var product = await http.GetFromJsonAsync<Product>("/api/products/12", Options);
var res = await http.PostAsJsonAsync("/api/products", new CreateProduct("Desk", 199), Options);
res.EnsureSuccessStatusCode();
var created = await res.Content.ReadFromJsonAsync<Product>(Options);
Newtonsoft.Json (Json.NET) when you meet it
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
var p = JsonConvert.DeserializeObject<Product>(json);
string text = JsonConvert.SerializeObject(p, Formatting.Indented, new JsonSerializerSettings {
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore,
});
[JsonProperty("created_at")] [JsonIgnore]
JObject o = JObject.Parse(json); string name = (string)o["customer"]!["name"]!; o["paid"] = true;
var skus = o.SelectTokens("$.items[*].sku"); // JSONPath built in
Go, Rust & Ruby
encoding/json, serde, the JSON gem, and a few more
Go: encoding/json
Struct tags do all the work
type Product struct {
ID int64 `json:"id"`
Name string `json:"name"`
Tags []string `json:"tags,omitempty"`
Price *float64 `json:"price,omitempty"` // pointer: nil means absent, 0 is a real price
CreatedAt time.Time `json:"created_at"` // RFC 3339 by default
Secret string `json:"-"` // never serialised
internal string // unexported: ignored
}
var p Product
if err := json.Unmarshal(data, &p); err != nil { return err }
out, err := json.Marshal(p)
pretty, _ := json.MarshalIndent(p, "", " ")
Streams, maps, and raw fragments
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields() // strict input
if err := dec.Decode(&body); err != nil { http.Error(w, err.Error(), 400); return }
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"ok": true})
var m map[string]any // when the shape is unknown; numbers become float64
json.Unmarshal(data, &m)
dec.UseNumber() // keep numbers as json.Number (a string) instead
type Event struct {
Type string `json:"type"`
Payload json.RawMessage `json:"payload"` // defer parsing until you know the type
}
Custom marshalling and the new v2 package
func (m Money) MarshalJSON() ([]byte, error) {
return json.Marshal(struct{ Amount string `json:"amount"`; Currency string `json:"currency"` }{fmt.Sprintf("%.2f", float64(m.Cents)/100), m.Currency})
}
func (m *Money) UnmarshalJSON(b []byte) error { ... }
// also: encoding.TextMarshaler for map keys and simple types
// encoding/json/v2 (experimental in Go 1.25 behind GOEXPERIMENT=jsonv2): faster, case sensitive by default,
// omitzero, `format:` tags, streaming Marshal/Unmarshal with io.Reader/io.Writer
Rust: serde_json
Derive it
// Cargo.toml: serde = { version = "1", features = ["derive"] }, serde_json = "1"
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct Product {
id: u64,
name: String,
#[serde(default)] tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")] price: Option<f64>,
#[serde(rename = "created_at", with = "time::serde::rfc3339")] created_at: time::OffsetDateTime,
}
let p: Product = serde_json::from_str(text)?;
let out = serde_json::to_string(&p)?;
let pretty = serde_json::to_string_pretty(&p)?;
let list: Vec<Product> = serde_json::from_reader(file)?;
Untyped: serde_json::Value and the json! macro
use serde_json::{json, Value};
let v: Value = serde_json::from_str(text)?;
let name = v["customer"]["name"].as_str().unwrap_or("unknown"); // indexing never panics, returns Null
if let Some(items) = v.get("items").and_then(Value::as_array) { for it in items { it["sku"].as_str(); } }
let doc = json!({ "ok": true, "tags": ["a", "b"], "count": 3 });
let typed: Product = serde_json::from_value(doc)?;
// enums: #[serde(tag = "type")] for a discriminator, #[serde(untagged)] to try variants in order
Ruby, Swift, Dart, Elixir
Ruby: the json gem, string keys by default
require "json"
data = JSON.parse('{"id": 12, "tags": ["a", "b"]}') # string keys
data = JSON.parse(text, symbolize_names: true) # symbol keys
JSON.generate(data) data.to_json JSON.pretty_generate(data)
JSON.parse(text, max_nesting: 20)
rescue JSON::ParserError => e
# Rails: render json: @product, status: :created; params are already parsed; Oj gem for speed
# Struct/Data + to_h for shaping; ActiveModel::Serializers, Jbuilder, Blueprinter for API output
Swift: Codable
struct Product: Codable {
let id: Int
let name: String
let tags: [String]
let createdAt: Date
enum CodingKeys: String, CodingKey { case id, name, tags, createdAt = "created_at" }
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
decoder.keyDecodingStrategy = .convertFromSnakeCase // or CodingKeys, not both
let product = try decoder.decode(Product.self, from: data)
let encoder = JSONEncoder(); encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let out = try encoder.encode(product)
let any = try JSONSerialization.jsonObject(with: data) as? [String: Any]
Dart and Elixir
// Dart
import 'dart:convert';
final data = jsonDecode(text) as Map<String, dynamic>;
final out = jsonEncode({'id': 12, 'tags': ['a']});
const JsonEncoder.withIndent(' ').convert(data);
// json_serializable: @JsonSerializable() class Product { factory Product.fromJson(Map<String, dynamic> j) => _$ProductFromJson(j); }
# Elixir: Jason (and JSON in the standard library since Elixir 1.18)
{:ok, data} = Jason.decode(text) # string keys
data = Jason.decode!(text, keys: :atoms) # only for trusted input; atoms are never garbage collected
Jason.encode!(%{id: 12, tags: ["a"]})
@derive {Jason.Encoder, only: [:id, :name]} # on a struct
jq
the command line JSON processor, from dot to reduce
Basics
Paths, iteration, and raw output
jq '.' order.json # pretty print (also validates) jq '.customer.name' order.json # "Ana Popescu" jq '.items[].sku' order.json # one value per line jq '.items | length' order.json # 2 jq -r '.customer.name' order.json # raw: Ana Popescu, no quotes jq '.items[0]' jq '.items[-1]' jq '.items[1:3]' jq '.["first name"]' jq '.missing' # null, no error jq '.missing?' jq '.items[]?' # suppress errors on the wrong type curl -s https://api.example/products | jq '.data[] | .name'
select, map, and building new objects
jq '[.items[] | select(.price > 100) | .sku]' order.json # filter into an array
jq '.items | map(.qty * .price)' order.json # [199, 50.5]
jq '.items[] | {sku, total: (.qty * .price)}' order.json # new objects, {sku} is shorthand for {sku: .sku}
jq -c '.items[] | {sku, total: (.qty * .price)}' order.json # -c: one compact object per line
jq '.items | map(select(.qty > 1))'
jq 'select(.status == "shipped" and .paid)'
jq '.items[] | select(.sku | test("^OAK"))' # regex
Change, add and remove keys
jq '.status = "delivered"' order.json # set
jq '.note = "left at door"' # add
jq '.items[0].qty += 1' # update in place
jq 'del(.coupon, .items)' # remove
jq '.customer |= {name}' # replace with a projection of itself
jq '. + {"source": "web"}' jq '. * {"customer": {"vip": true}}' # shallow merge, deep merge
jq 'with_entries(select(.value != null))' # drop null valued keys
jq 'to_entries | map(.key)' jq 'keys' jq 'keys_unsorted' jq 'has("paid")'
Aggregation and Reshaping
Sums, groups, counts
jq '[.items[] | .qty * .price] | add' # 249.5
jq 'group_by(.status) | map({status: .[0].status, n: length})' orders.json # counts per status
jq 'group_by(.status) | map({(.[0].status): length}) | add' # as an object
jq 'map(.price) | min, max, (add / length)' # min, max, mean
jq 'sort_by(.price) | reverse' jq 'sort_by(.status, .id)' jq 'unique_by(.sku)'
jq 'reduce .items[] as $i (0; . + $i.qty)' # a fold
jq '[.[] | .tags[]] | unique' jq 'flatten' jq '[limit(3; .[])]' jq 'first, last' jq 'any(.paid)'
Strings, CSV and TSV out
jq -r '.items[] | "\(.sku) x\(.qty) = \(.qty * .price)"' order.json # string interpolation
jq -r '.items[] | [.sku, .qty] | @csv' jq -r '@tsv' jq -r '@sh' jq -r '@base64' jq -r '@uri' jq -r '@html'
jq -r '["sku","qty"], (.items[] | [.sku, .qty]) | @csv' # header row first
jq '.name | ascii_downcase | split(" ") | join("-")'
jq '.sku | sub("-"; "_")' jq 'gsub("[^a-z]"; "")' jq 'ltrimstr("OAK-")' jq 'startswith("OAK")'
jq '.created_at | fromdateiso8601' jq 'now | todate' jq '.ts | strftime("%Y-%m-%d")'
Input handling: files, streams, arguments
jq -s 'add' a.json b.json # -s slurp: all inputs into one array first
jq -n '[inputs]' log.jsonl # -n null input, read JSON Lines into an array
jq -c '.' big.jsonl | head # one document per line in, one per line out
jq --arg name "Ana" '.customer.name = $name' # string argument
jq --argjson n 3 '.qty = $n' # JSON argument
jq --slurpfile cfg config.json '.cfg = $cfg[0]'
jq -r '.token' < response.json | xargs -I{} curl -H "Authorization: Bearer {}" https://api.example/me
jq -e '.ok' response.json && echo "ok" # -e: exit status from the last value (false/null = 1)
jq -S '.' # sort keys; --tab, --indent 4; -j no newline; -M no colour; -C force colour
Paths, walk, recursion, and functions
jq -c 'paths(type == "string")' order.json # every path to a string leaf
jq 'getpath(["customer","name"])' jq 'setpath(["a","b"]; 1)' jq 'leaf_paths'
jq 'walk(if type == "string" then ascii_upcase else . end)' # apply to every value at every depth
jq '.. | .sku? // empty' # recursive descent, like JSONPath ..
jq '[.. | numbers]' jq '[.. | select(type == "object" and has("sku"))]'
jq 'def total: .qty * .price; .items | map(total)' # define a function
jq 'if .paid then "paid" elif .status == "cancelled" then "void" else "due" end'
jq '.price // 0' jq '.name // "unknown"' # alternative operator: null/false fallback
jq 'try (.a | tonumber) catch "not a number"' jq '.x | tostring' jq 'tojson' jq 'fromjson' jq '@json'
jq 'env.HOME' jq '$ENV.PATH' jq 'input_filename' jq '$__loc__' jq 'debug' jq 'error("bad")'
Command Line & Editors
curl, json.tool, gron, fx, VS Code, diffs
Shell Tools
curl: send JSON, read JSON
curl -s -X POST https://api.example/products \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"name": "Desk", "price": 199}'
curl -s --json '{"name": "Desk"}' https://api.example/products # curl 7.82+: sets both headers
curl -s -d @body.json -H "Content-Type: application/json" https://api.example/products
curl -s https://api.example/products | jq '.data[0]'
curl -si https://api.example/products/12 | head -20 # -i shows status and headers
curl -s -o /dev/null -w "%{http_code} %{content_type}\n" https://api.example/products
Pretty print and validate with what is already installed
python -m json.tool file.json # pretty print, exit 1 with position on error
python -m json.tool --sort-keys --indent 2 in.json out.json
python -m json.tool --json-lines log.jsonl # each line separately
python -m json.tool --no-ensure-ascii file.json # keep é instead of \u00e9
node -e 'console.log(JSON.stringify(JSON.parse(require("fs").readFileSync(0,"utf8")),null,2))' < file.json
jq . file.json # the usual answer
php -r 'echo json_encode(json_decode(file_get_contents("php://stdin")), JSON_PRETTY_PRINT);' < file.json
gron: make JSON greppable
gron order.json | grep sku gron order.json | grep -i "ana" # find the path to a value gron order.json | grep items | gron -u # back to JSON, only the matching part curl -s https://api.example/me | gron
More tools worth installing
fx order.json # interactive viewer with JavaScript filters: fx order.json '.items.map(x => x.sku)'
jless order.json # terminal JSON viewer with folding and search
jo name=Desk price=199 tags='["a","b"]' # build JSON from arguments: {"name":"Desk","price":199,"tags":["a","b"]}
jo -a a b c # ["a","b","c"]
yq -p json -o yaml order.json # convert JSON to YAML (mikefarah yq), also TOML, XML, CSV
yq -o json config.yaml # YAML to JSON
dasel -f order.json '.customer.name' # one tool for JSON, YAML, TOML, XML, CSV
jsonlint file.json # npm i -g jsonlint
npx prettier --write "**/*.json" # format every JSON file in a repo
miller / mlr --ijson --ocsv cat data.json # JSON to CSV for spreadsheets
Editors and Diffs
VS Code settings for JSON files
{
"[json]": { "editor.defaultFormatter": "vscode.json-language-features", "editor.formatOnSave": true },
"[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features" },
"json.schemas": [
{ "fileMatch": ["/config/*.json"], "url": "./schemas/config.schema.json" }
],
"files.associations": { "*.json5": "json5", ".eslintrc": "jsonc", "tsconfig*.json": "jsonc" },
"json.maxItemsComputed": 10000
}
// or declare it in the file itself
{ "$schema": "https://json.schemastore.org/package.json", "name": "shop" }
Keyboard: format, fold, sort, select
Shift+Alt+F format document (Shift+Option+F on macOS)
Ctrl+K Ctrl+0 fold all; Ctrl+K Ctrl+J unfold all; Ctrl+Shift+[ fold the current block
Ctrl+Shift+P "Sort JSON" via the Sort JSON objects extension
Ctrl+Shift+P "JSON: Minify" via the JSON Tools extension
Alt+Click multiple cursors, for editing many keys at once
Ctrl+D add the next match to the selection ("id": on every line)
Ctrl+Shift+V markdown preview; for JSON use the JSON Crack or JSON Viewer extension for a tree
Diff two JSON documents meaningfully
diff <(jq -S . a.json) <(jq -S . b.json) # normalise, then diff git diff --no-index <(jq -S . a.json) <(jq -S . b.json) npx json-diff a.json b.json # coloured semantic diff jd a.json b.json # jd: outputs a diff, or a JSON Patch with -f patch python -c 'import json,sys; from deepdiff import DeepDiff; print(DeepDiff(json.load(open(sys.argv[1])), json.load(open(sys.argv[2]))))' a.json b.json # git: treat minified JSON as text, and keep it diffable *.json diff=json # .gitattributes, then git config diff.json.textconv "jq -S ."
JSON Schema
describe, validate and document a document
A Complete Schema
The keywords you will use on every schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://api.shop.example/schemas/product.json",
"title": "Product",
"description": "A product in the catalogue.",
"type": "object",
"required": ["id", "name", "price", "status"],
"additionalProperties": false,
"properties": {
"id": { "type": "integer", "minimum": 1 },
"name": { "type": "string", "minLength": 1, "maxLength": 120 },
"slug": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
"price": { "type": "number", "minimum": 0, "multipleOf": 0.01 },
"currency": { "type": "string", "enum": ["EUR", "USD", "GBP"], "default": "EUR" },
"status": { "enum": ["draft", "live", "archived"] },
"tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true, "maxItems": 20 },
"email": { "type": "string", "format": "email" },
"created_at": { "type": "string", "format": "date-time" },
"coupon": { "type": ["string", "null"] },
"dimensions": { "$ref": "#/$defs/dimensions" },
"meta": { "type": "object", "additionalProperties": { "type": "string" } }
},
"$defs": {
"dimensions": {
"type": "object",
"properties": { "w": { "type": "number" }, "h": { "type": "number" }, "unit": { "const": "cm" } },
"required": ["w", "h"]
}
},
"examples": [{ "id": 12, "name": "Oak desk", "price": 199, "status": "live" }]
}
Keywords by Type
| Applies to | Keywords |
|---|---|
| any | type (or an array of types), enum, const, default, examples, title, description, deprecated, readOnly, writeOnly |
| string | minLength, maxLength, pattern (ECMA regex, unanchored), format, contentEncoding, contentMediaType |
| number, integer | minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf |
| object | properties, required, additionalProperties, patternProperties, propertyNames, minProperties, maxProperties, dependentRequired, dependentSchemas, unevaluatedProperties |
| array | items, prefixItems (tuples), contains, minContains, minItems, maxItems, uniqueItems, unevaluatedItems |
| combining | allOf, anyOf, oneOf, not, if / then / else |
| structure | $schema, $id, $ref, $defs, $anchor, $dynamicRef, $comment |
Formats, and the catch
"format": "date-time" "date" "time" "duration" "email" "hostname" "ipv4" "ipv6" "format": "uri" "uri-reference" "uuid" "regex" "json-pointer" "relative-json-pointer" "iri" "idn-email" # custom formats are allowed and ignored by validators that do not know them
Conditionals, polymorphism, tuples
// one of several shapes, chosen by a discriminator
"oneOf": [ { "$ref": "#/$defs/card" }, { "$ref": "#/$defs/bank" } ]
// $defs/card: { "properties": { "type": { "const": "card" }, "last4": {...} }, "required": ["type", "last4"] }
// if / then / else
"if": { "properties": { "country": { "const": "US" } } },
"then": { "required": ["zip"] },
"else": { "required": ["postal_code"] }
// a fixed length tuple, and nothing after it
"type": "array", "prefixItems": [ { "type": "number" }, { "type": "number" } ], "items": false
// require b when a is present
"dependentRequired": { "credit_card": ["billing_address"] }
Validate in Code
JavaScript: Ajv
import Ajv2020 from "ajv/dist/2020";
import addFormats from "ajv-formats";
const ajv = new Ajv2020({allErrors: true, strict: true});
addFormats(ajv);
const validate = ajv.compile(schema); // compile once, reuse; it is a generated function
if (!validate(data)) { console.log(validate.errors); }
ajv.validate(schema, data); // one shot, cached by schema object
Python: jsonschema
pip install jsonschema
from jsonschema import Draft202012Validator, validate, ValidationError
validate(instance=data, schema=schema) # raises ValidationError
validator = Draft202012Validator(schema, format_checker=Draft202012Validator.FORMAT_CHECKER)
for error in sorted(validator.iter_errors(data), key=lambda e: e.path):
print(list(error.path), error.message)
Other languages and the command line
check-jsonschema --schemafile product.schema.json data.json # pip install check-jsonschema, also a pre-commit hook npx ajv-cli validate -s product.schema.json -d "data/*.json" # Java: networknt json-schema-validator; .NET: JsonSchema.Net; Go: santhosh-tekuri/jsonschema; Rust: jsonschema crate # PHP: opis/json-schema; Ruby: json_schemer; Kotlin: kotlinx.serialization + kjson-schema # generate a schema from data or from types npx quicktype data.json --lang schema -o data.schema.json pydantic: Product.model_json_schema() zod: npm i zod-to-json-schema TypeScript: ts-json-schema-generator
OpenAPI: the same schemas describing an API
openapi: 3.1.0
components:
schemas:
Product:
$ref: "./product.schema.json" # a 2020-12 schema, unchanged
paths:
/products/{id}:
get:
responses:
"200":
content:
application/json:
schema: { $ref: "#/components/schemas/Product" }
Pointer, Patch & JSONPath
address a value, describe a change, query a document
JSON Pointer
Syntax and escaping
"" the whole document "/customer/name" "Ana Popescu" "/items/0/sku" "OAK-1" array indexes are plain numbers "/items/-" the position after the last element, for add "/a~1b" the key "a/b" / is escaped as ~1 "/m~0n" the key "m~n" ~ is escaped as ~0 "/" the key "" (an empty key, not the root) "#/$defs/dimensions" as a URI fragment, in JSON Schema $ref
Resolve one
// JavaScript
const get = (doc, ptr) => ptr.split("/").slice(1)
.map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"))
.reduce((acc, key) => (acc == null ? undefined : acc[key]), doc);
get(order, "/items/0/sku"); // "OAK-1"
# Python
pip install jsonpointer; from jsonpointer import resolve_pointer; resolve_pointer(order, "/items/0/sku")
# jq
jq 'getpath(["items", 0, "sku"])'
JSON Patch and Merge Patch
JSON Patch: an ordered list of operations
PATCH /orders/10042
Content-Type: application/json-patch+json
[
{ "op": "test", "path": "/status", "value": "shipped" },
{ "op": "replace", "path": "/status", "value": "delivered" },
{ "op": "add", "path": "/note", "value": "left at door" },
{ "op": "add", "path": "/items/-", "value": { "sku": "RUG-9", "qty": 1 } },
{ "op": "replace", "path": "/items/0/qty", "value": 2 },
{ "op": "remove", "path": "/coupon" },
{ "op": "move", "from": "/customer/email", "path": "/contact_email" },
{ "op": "copy", "from": "/customer/name", "path": "/shipping/name" }
]
JSON Merge Patch: send what changed
PATCH /orders/10042
Content-Type: application/merge-patch+json
{ "status": "delivered", "note": "left at door", "coupon": null, "customer": { "vip": true } }
# result: status and note set, coupon removed, customer.vip added, everything else untouched
# arrays are replaced whole: "items": [...] overwrites the list
# cannot express "set coupon to null": null means remove
Apply and generate patches
// JavaScript: fast-json-patch
import {applyPatch, compare} from "fast-json-patch";
const patch = compare(before, after); // generates the operations
const {newDocument} = applyPatch(structuredClone(doc), patch, true); // validate = true
# Python
pip install jsonpatch; jsonpatch.apply_patch(doc, patch); jsonpatch.make_patch(before, after)
pip install json-merge-patch; json_merge_patch.merge(doc, patch)
# jq does merge patch natively
jq '. * {"status": "delivered", "customer": {"vip": true}} | del(.coupon)'
# kubectl uses all three: kubectl patch --type json / merge / strategic
JSONPath
Syntax, standardised in 2024
$ the root $.customer.name dot child $['first name'] bracket child, for keys with spaces or quotes $.items[0] index; $.items[-1] last; $.items[0:2] slice; $.items[0,2] union $.items[*] every element; $.* every member $..sku recursive descent: sku at any depth $.items[?@.price > 100] filter; @ is the current element $.items[?@.sku == 'OAK-1' || @.qty > 1] $.items[?match(@.sku, "OAK-.*")] function extensions: length(), count(), match(), search(), value() $.items[?@.coupon] exists
Libraries and where JSONPath shows up
JavaScript npm i jsonpath-plus; JSONPath({path: "$.items[*].sku", json: order})
Python pip install jsonpath-ng; parse("$.items[*].sku").find(order); python-jsonpath for RFC 9535
Java com.jayway.jsonpath; JsonPath.read(json, "$.items[?(@.price > 100)].sku")
.NET JObject.SelectTokens("$..sku"); JsonPath.Net for RFC 9535
Go github.com/ohler55/ojg/jp; PaesslerAG/jsonpath
Postgres jsonb_path_query(doc, '$.items[*] ? (@.price > 100).sku') SQL/JSON path, close cousin
MySQL JSON_EXTRACT(doc, '$.items[*].sku')
Kubernetes kubectl get pods -o jsonpath='{.items[*].metadata.name}'
Postman pm.response.json() then a JS path; tests use jsonpath via lodash
# jq is not JSONPath: .items[] | select(.price > 100) | .sku
Spreadsheets, Postman & Automation
JSON without writing a program
Excel and Google Sheets
Excel: Data, Get Data, From JSON (Power Query)
Data > Get Data > From File > From JSON (or From Web with an API URL)
then: To Table > expand the Record column > pick the fields > Close & Load
// the M code it writes, editable in the Advanced Editor
let
Source = Json.Document(Web.Contents("https://api.example/products")),
data = Source[data],
tbl = Table.FromList(data, Splitter.SplitByNothing(), null, null, ExtraValues.Error),
cols = Table.ExpandRecordColumn(tbl, "Column1", {"id", "name", "price", "tags"})
in
cols
// headers and auth
Json.Document(Web.Contents("https://api.example/me", [Headers = [Authorization = "Bearer " & token, Accept = "application/json"]]))
Google Sheets: Apps Script, since there is no built in JSON function
// Extensions > Apps Script, paste, save, then use =IMPORTJSON(url, path) in a cell
function IMPORTJSON(url, path) {
const data = JSON.parse(UrlFetchApp.fetch(url, {headers: {Accept: "application/json"}}).getContentText());
const rows = Array.isArray(data) ? data : (data.data || [data]);
const keys = Object.keys(rows[0]);
return [keys].concat(rows.map((r) => keys.map((k) => typeof r[k] === "object" ? JSON.stringify(r[k]) : r[k])));
}
// or the community ImportJSON.gs script by Brad Jasper for path syntax like "/data/name"
// export a sheet as JSON: Apps Script with SpreadsheetApp.getActiveSheet().getDataRange().getValues(), or File > Download > CSV then convert
Flattening rules, and converting in either direction
{"id": 12, "customer": {"name": "Ana"}, "tags": ["a", "b"]}
flattened, one row: id | customer.name | tags
12 | Ana | a;b
exploded, one row per tag: id | customer.name | tag
12 | Ana | a
12 | Ana | b
# tools that do it for you
mlr --ijson --ocsv --jflatsep . cat data.json # Miller flattens with dotted names
jq -r '.[] | [.id, .customer.name, (.tags | join(";"))] | @csv'
python: pd.json_normalize(records, record_path="tags", meta=["id", ["customer", "name"]])
csvjson data.csv (csvkit), or https://www.convertcsv.com style tools for a one off
Postman, Insomnia, Bruno, Swagger
Postman: send JSON, test the response, chain requests
// Body tab: raw, JSON; Headers: Content-Type is set automatically for raw JSON
{"name": "Desk", "price": {{price}}} // {{variables}} from the environment
// Scripts > Post-response
const body = pm.response.json();
pm.test("status is 201", () => pm.response.to.have.status(201));
pm.test("body has an id", () => pm.expect(body.id).to.be.a("number"));
pm.test("price is a number", () => pm.expect(body.price).to.eql(199));
pm.test("matches schema", () => pm.response.to.have.jsonSchema(schema)); // Ajv under the hood
pm.environment.set("productId", body.id); // use it in the next request as {{productId}}
pm.expect(pm.response.headers.get("Content-Type")).to.include("application/json");
// Pre-request: build a body
pm.request.body.raw = JSON.stringify({name: "Desk", nonce: Date.now()});
// run a whole collection from the terminal: npx newman run collection.json -e env.json
Insomnia, Bruno, HTTPie, and .http files
# Bruno: requests are text files in git, JSON body inline
post {
url: {{base}}/products
body: json
}
body:json {
{ "name": "Desk", "price": 199 }
}
assert { res.status: eq 201 res.body.id: isNumber }
# Insomnia: same idea, response filter box takes a JSONPath ($.data[*].name) to narrow the preview
# HTTPie: JSON is the default body type
http POST api.example/products name=Desk price:=199 tags:='["a","b"]' # := sends raw JSON, = sends a string
# VS Code REST Client / JetBrains HTTP client: a .http file
POST https://api.example/products
Content-Type: application/json
{"name": "Desk", "price": 199}
Swagger UI and Redoc: try an API from its OpenAPI file
https://api.example/docs # FastAPI, NestJS, Spring (springdoc), ASP.NET Core (Swashbuckle) serve it by default https://api.example/openapi.json # the document itself, JSON Try it out > edit the example body > Execute > see the curl, the request URL, the response body and headers Schema tab on any response: the JSON Schema for the body, with required fields marked npx @redocly/cli preview-docs openapi.yaml # Redoc, a read only rendering npx swagger-cli validate openapi.yaml # check the document; also Spectral for linting
Zapier, Make, n8n
Where JSON shows up in each tool
Zapier Webhooks by Zapier: Catch Hook parses the body into fields; Custom Request sends raw JSON with a Content-Type header
nested: "customer__name" style field names; arrays become line items; Formatter > Utilities > Line Itemizer
Code by Zapier: return {name: inputData.body.customer.name}; JSON.parse(inputData.raw) for a string field
Make HTTP module > Make a request > Body type: Raw, Content type: JSON; JSON module: Parse JSON with a data structure
mapping: {{1.data.customer.name}}; Iterator over an array, Aggregator back into one; Create JSON module builds a body
n8n Webhook node exposes $json; HTTP Request node with Body Content Type JSON; expressions: {{ $json.customer.name }}
Code node (JavaScript or Python): return items.map(i => ({json: {name: i.json.customer.name}}));
Split Out node for arrays; Aggregate node to combine; Edit Fields (Set) node to build a JSON object
A webhook payload, and the field paths each tool derives
{"event": "order.paid", "data": {"id": 10042, "total": 249.5, "items": [{"sku": "OAK-1", "qty": 1}]}}
Zapier Data Id, Data Total, Data Items Sku (first only, or line items)
Make {{1.data.id}} {{1.data.items[].sku}} via an Iterator
n8n {{ $json.data.id }} {{ $json.data.items[0].sku }} {{ $json.data.items.map(i => i.sku).join(", ") }}
# test with a fixed sample first: paste the JSON into the tool's sample or use https://webhook.site to capture a real one
Airtable, Notion and other API first tools
# Airtable: records are {"id": "rec...", "fields": {"Name": "Desk", "Price": 199}}; field names are the JSON keys, spaces included
curl "https://api.airtable.com/v0/BASE/Products?maxRecords=3" -H "Authorization: Bearer $TOKEN" | jq '.records[].fields'
# Notion: every property is typed JSON: {"Name": {"title": [{"text": {"content": "Desk"}}]}}, so a text value is three levels deep
jq '.results[].properties.Name.title[0].plain_text'
# Google Forms / Typeform / Stripe webhooks: JSON in, a spreadsheet or a message out; the tools above are the glue
APIs & HTTP
headers, error shapes, conventions that survive
Headers and Status
The headers on every JSON request and response
POST /api/products HTTP/1.1
Content-Type: application/json
Accept: application/json
Content-Length: 34
{"name": "Desk", "price": 199}
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/products/12
ETag: "a1b2c3"
{"id": 12, "name": "Desk", "price": 199}
# related media types
application/problem+json errors (RFC 9457) application/json-patch+json JSON Patch
application/merge-patch+json Merge Patch application/vnd.api+json JSON:API
application/x-ndjson JSON Lines application/ld+json JSON-LD
application/schema+json a JSON Schema text/event-stream SSE carrying JSON
Status codes that pair with a JSON body
200 OK a body with the resource or the list
201 Created the new resource, plus a Location header
204 No Content no body at all: do not send "null" or "{}"
400 Bad Request malformed JSON or a failed validation, say which in the body
401 / 403 not authenticated / not allowed
404 Not Found a JSON error body, not an HTML page
409 Conflict version mismatch, duplicate
415 Unsupported Media Type the client sent something that is not application/json
422 Unprocessable Content valid JSON, invalid meaning (many frameworks use this for validation)
429 Too Many Requests with Retry-After
500 a generic JSON error, never a stack trace
One error shape: Problem Details
{
"type": "https://api.shop.example/problems/validation",
"title": "Validation failed",
"status": 422,
"detail": "price must be a non negative number",
"instance": "/api/products",
"errors": [
{ "pointer": "/price", "message": "must be >= 0" },
{ "pointer": "/name", "message": "is required" }
]
}
# the minimum for any API: a stable machine code plus a human message
{"error": {"code": "validation_failed", "message": "price must be a non negative number"}}
Conventions
Naming: pick one, everywhere
{"createdAt": "...", "orderItems": []} camelCase: JavaScript clients, Google JSON style guide, JSON:API allows it
{"created_at": "...", "order_items": []} snake_case: Python, Ruby, Rails, Stripe, GitHub, most SQL
rules that hold either way:
keys are lowercase first letter, no spaces, no leading $ or _ (reserved by some tools)
plural for arrays: "items", singular for objects: "customer"
booleans read as questions: "is_active", "has_children", "paid"
ids are strings when they can exceed 2^53 or start with 0: "id": "10042"
do not encode types in names: "price_float", "tags_array"
Dates, money, nulls, empties, enums
dates ISO 8601 with a zone: "2026-09-21T09:04:11Z"; durations as ISO 8601 or integer seconds with the unit in the name: "timeout_seconds" money integer minor units + currency, or a decimal string; never a float absent omit the key for "not applicable"; send null for "cleared"; document which empty [] not null for "no items"; "" only when empty string is a real value enums lowercase strings, "status": "shipped", never magic numbers booleans true/false, never "yes"/"no" or 1/0 ids stable, opaque, string if in doubt; expose UUIDs rather than auto increment counters when they leak information versions in the URL /v2/ or a header; add fields freely, never rename or retype one without a new version
Collections: envelope, pagination, filtering, sparse fields
GET /api/products?status=live&sort=-created_at&page[size]=25&page[cursor]=eyJpZCI6MTV9&fields=id,name,price
{
"data": [ {"id": "12", "name": "Oak desk", "price": 199} ],
"meta": { "total": 128, "page_size": 25 },
"links": { "self": "/api/products?page[cursor]=...", "next": "/api/products?page[cursor]=..." }
}
# cursor pagination scales; page numbers are simpler and fine under a few thousand rows
# a top level array works but leaves no room for meta; wrap it from day one
Full conventions: JSON:API, HAL, JSON-LD
// JSON:API (application/vnd.api+json)
{"data": {"type": "products", "id": "12", "attributes": {"name": "Oak desk"},
"relationships": {"category": {"data": {"type": "categories", "id": "3"}}}},
"included": [{"type": "categories", "id": "3", "attributes": {"name": "Desks"}}]}
// HAL (application/hal+json)
{"id": 12, "name": "Oak desk", "_links": {"self": {"href": "/products/12"}, "category": {"href": "/categories/3"}}}
// JSON-LD (application/ld+json): JSON with a vocabulary, what schema.org markup on this page uses
{"@context": "https://schema.org", "@type": "Product", "name": "Oak desk", "offers": {"@type": "Offer", "price": "199"}}
Webhooks, idempotency, compression, caching
webhooks POST a JSON event with "id", "type", "created_at", "data"; sign the raw body (HMAC), verify before parsing; retry with backoff
idempotency Idempotency-Key: 7d3f... on POST; same key + same body = same response, no double charge
compression Accept-Encoding: gzip, br; JSON shrinks 5 to 10x; enable it at the proxy, not in code
caching ETag on GET, If-None-Match on the next request, 304 with no body; Cache-Control: private, max-age=60
streaming JSON Lines over a chunked response, or Server-Sent Events with one JSON object per event
GraphQL always POST {"query": "...", "variables": {...}} and always 200 with {"data": ..., "errors": [...]}
Rate Limits, Retries, Field Masks, GraphQL
A rate limit response and a client that respects it
async function getJson(url, tries = 4) {
for (let i = 0; i < tries; i++) {
const res = await fetch(url, {headers: {Accept: "application/json"}});
if (res.status === 429 || res.status >= 500) {
const wait = Number(res.headers.get("Retry-After") ?? 2 ** i) * 1000 + Math.random() * 500;
await new Promise((r) => setTimeout(r, wait));
continue;
}
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
return res.json();
}
throw new Error("gave up");
}
# Python: pip install tenacity; @retry(wait=wait_exponential_jitter(), stop=stop_after_attempt(4), retry=retry_if_exception_type(RateLimited))
Field masks and partial responses
?fields=id,name,price flat list (JSON:API: fields[products]=name,price) ?fields=id,customer(name,email) nested, Google style; Google APIs also accept ?fields=items/id,items/name ?include=customer,items sideload related resources instead of nesting ?expand=customer Stripe style: replace an id with the object Prefer: return=minimal on POST/PATCH: answer 204 instead of echoing the resource Prefer: return=representation the opposite; Preference-Applied tells you which happened // server side: parse the mask into an allow list, apply after loading, never build SQL from it
GraphQL: one request shape, one response shape
POST /graphql
Content-Type: application/json
{"query": "query Product($id: ID!) { product(id: $id) { id name price category { name } } }",
"variables": {"id": "12"},
"operationName": "Product"}
// always check both keys
const {data, errors} = await res.json();
if (errors?.length) { log(errors); } // partial data may still be usable
// scalars: ID is a string, Int is 32 bit, Float is a double; custom scalars (DateTime, JSON, BigInt) are strings or numbers by convention
JSON Lines, JSON5 & JSONC
the supersets and siblings you will meet
JSON Lines / NDJSON
One document per line
{"ts": "2026-09-21T09:04:11Z", "level": "info", "msg": "started", "port": 8080}
{"ts": "2026-09-21T09:04:12Z", "level": "warn", "msg": "slow query", "ms": 1830}
{"ts": "2026-09-21T09:04:15Z", "level": "error", "msg": "timeout", "upstream": "db"}
# rules: UTF-8, \n between records, no trailing comma, no outer [ ], each line is valid JSON
# media type application/x-ndjson (or application/jsonl); extension .jsonl or .ndjson
Process it: jq, grep, Python, Node
jq -c 'select(.level == "error")' app.jsonl # filter, keep one per line
jq -s '.' app.jsonl > app.json # slurp into one array
jq -c '.[]' app.json > app.jsonl # the other way
jq -r '.level' app.jsonl | sort | uniq -c # counts
grep '"level": "error"' app.jsonl | wc -l # plain grep works because each record is a line
tail -f app.jsonl | jq -c 'select(.ms > 1000)' # live
# Python
with open("app.jsonl", encoding="utf-8") as f:
for line in f:
if line.strip(): record = json.loads(line)
# Node
for await (const line of readline.createInterface({input: fs.createReadStream("app.jsonl")})) { if (line) rows.push(JSON.parse(line)); }
Where it is the native format
Elasticsearch bulk API action line, then document line, repeated; Content-Type: application/x-ndjson
BigQuery, Snowflake, Athena load and export newline delimited JSON
OpenAI / LLM fine tuning one {"messages": [...]} per line
Docker, Kubernetes, pino structured logs, one event per line
Hugging Face datasets .jsonl splits
# related: JSON text sequences (RFC 7464) use a record separator byte instead of newline; rare
JSON5, JSONC, HJSON
JSON5: what it adds
{
// single and /* block */ comments
unquoted: 'single quoted',
'trailing': [1, 2, 3,],
hex: 0xFF,
half: .5,
whole: 5.,
positive: +1,
inf: Infinity, nan: NaN,
multi: 'first \
second',
$special_keys: true, _under: true,
}
npm i json5 JSON5.parse(text) JSON5.stringify(obj, null, 2)
pip install json5 json5.loads(text)
used by: Babel (.babelrc), Chromium, Bun, some Rust tools; tsconfig and VS Code use JSONC, not JSON5
JSONC: JSON with comments, the editor dialect
// tsconfig.json, .vscode/settings.json, .devcontainer.json, .eslintrc.json (some), Deno's deno.jsonc
{
"compilerOptions": {
"target": "ES2022", // trailing comma below is tolerated by VS Code and tsc
"strict": true,
}
}
npm i jsonc-parser parse(text, errors, {allowTrailingComma: true}) // the VS Code parser
npm i strip-json-comments JSON.parse(stripJsonComments(text))
python: pip install jsonc-parser or json5 (a superset that reads JSONC)
HJSON, and the siblings that are still plain JSON
# HJSON: quotes and commas optional, comments allowed; rare outside a few config files
{
name: shop-api
port: 8080
hosts: [api.shop.example, admin.shop.example]
}
# these ARE valid JSON with conventions on top:
GeoJSON {"type": "Feature", "geometry": {"type": "Point", "coordinates": [26.10, 44.43]}, "properties": {...}}
JSON-LD {"@context": "https://schema.org", "@type": "Product", "name": "Oak desk"}
JSON:API {"data": {"type": "products", "id": "12", "attributes": {...}}}
JWT base64url(header).base64url(payload).signature, where header and payload are JSON objects
JSON-RPC {"jsonrpc": "2.0", "method": "sum", "params": [1, 2], "id": 1}
Databases
PostgreSQL jsonb, MySQL, SQLite, SQL Server, MongoDB
PostgreSQL jsonb
Store, read, and dig with operators
CREATE TABLE orders (id bigint PRIMARY KEY, doc jsonb NOT NULL);
INSERT INTO orders VALUES (10042, '{"status": "shipped", "customer": {"name": "Ana"}, "items": [{"sku": "OAK-1", "qty": 1}, {"sku": "LAMP-3", "qty": 2}]}');
SELECT doc -> 'customer' ->> 'name' FROM orders; -- 'Ana' (-> jsonb, ->> text)
SELECT doc #>> '{customer,name}' FROM orders; -- path form
SELECT doc -> 'items' -> 0 ->> 'sku' FROM orders; -- 'OAK-1'
SELECT doc['customer']['name'] FROM orders; -- subscript syntax, Postgres 14+
SELECT item ->> 'sku' AS sku, (item ->> 'qty')::int AS qty
FROM orders, jsonb_array_elements(doc -> 'items') AS item; -- one row per array element
Filter and index
SELECT * FROM orders WHERE doc @> '{"status": "shipped"}'; -- containment
SELECT * FROM orders WHERE doc @> '{"items": [{"sku": "OAK-1"}]}'; -- inside an array
SELECT * FROM orders WHERE doc ? 'coupon'; -- key exists
SELECT * FROM orders WHERE doc ?| array['coupon', 'note']; -- any key; ?& all keys
SELECT * FROM orders WHERE doc ->> 'status' = 'shipped';
SELECT * FROM orders WHERE (doc -> 'total')::numeric > 100;
SELECT * FROM orders WHERE doc @? '$.items[*] ? (@.qty > 1)'; -- SQL/JSON path exists
SELECT jsonb_path_query(doc, '$.items[*].sku') FROM orders;
CREATE INDEX orders_doc_idx ON orders USING gin (doc); -- @>, ?, ?|, ?&, @?
CREATE INDEX orders_doc_ops_idx ON orders USING gin (doc jsonb_path_ops); -- smaller, @> and @? only
CREATE INDEX orders_status_idx ON orders ((doc ->> 'status')); -- one key, btree
Update in place
UPDATE orders SET doc = doc || '{"status": "delivered", "note": "left at door"}'; -- shallow merge
UPDATE orders SET doc = jsonb_set(doc, '{customer,vip}', 'true'); -- set a path
UPDATE orders SET doc = jsonb_set(doc, '{items,0,qty}', '2');
UPDATE orders SET doc = doc - 'coupon'; -- remove a key
UPDATE orders SET doc = doc #- '{customer,email}'; -- remove a path
UPDATE orders SET doc = jsonb_insert(doc, '{items,-1}', '{"sku": "RUG-9"}', true); -- append after last
UPDATE orders SET doc = jsonb_strip_nulls(doc);
Build JSON from rows, and rows from JSON
SELECT jsonb_build_object('id', id, 'status', doc ->> 'status',
'skus', (SELECT jsonb_agg(i ->> 'sku') FROM jsonb_array_elements(doc -> 'items') i))
FROM orders;
SELECT jsonb_agg(to_jsonb(p)) FROM products p; -- a whole table as a JSON array
SELECT row_to_json(p) FROM products p;
SELECT jsonb_pretty(doc) FROM orders;
-- rows out of JSON, typed
SELECT * FROM jsonb_to_recordset('[{"sku": "OAK-1", "qty": 1}]') AS x(sku text, qty int);
SELECT * FROM json_table(doc, '$.items[*]' COLUMNS (sku text PATH '$.sku', qty int PATH '$.qty')) FROM orders; -- Postgres 17+
-- typed columns generated from JSON, for constraints and plain indexes
ALTER TABLE orders ADD COLUMN status text GENERATED ALWAYS AS (doc ->> 'status') STORED;
ALTER TABLE orders ADD CONSTRAINT doc_has_status CHECK (doc ? 'status');
MySQL, SQLite, SQL Server
MySQL 8 and MariaDB
CREATE TABLE orders (id BIGINT PRIMARY KEY, doc JSON NOT NULL);
SELECT doc->'$.customer.name', doc->>'$.customer.name' FROM orders; -- -> keeps quotes, ->> unquotes
SELECT JSON_EXTRACT(doc, '$.items[0].sku'), JSON_UNQUOTE(JSON_EXTRACT(doc, '$.status')) FROM orders;
SELECT * FROM orders WHERE JSON_CONTAINS(doc, '"shipped"', '$.status');
SELECT * FROM orders WHERE doc->>'$.status' = 'shipped';
UPDATE orders SET doc = JSON_SET(doc, '$.status', 'delivered', '$.note', 'left at door');
UPDATE orders SET doc = JSON_REMOVE(doc, '$.coupon');
SELECT * FROM orders, JSON_TABLE(doc, '$.items[*]' COLUMNS (sku VARCHAR(20) PATH '$.sku', qty INT PATH '$.qty')) AS items;
ALTER TABLE orders ADD COLUMN status VARCHAR(20) AS (doc->>'$.status') STORED, ADD INDEX (status); -- index via a generated column
SELECT JSON_PRETTY(doc), JSON_VALID('{"a": 1}'), JSON_ARRAYAGG(id), JSON_OBJECTAGG(id, doc->>'$.status') FROM orders;
SQLite
SELECT json_extract(doc, '$.customer.name'), doc ->> '$.status' FROM orders; -- ->> since 3.38
SELECT * FROM orders WHERE json_extract(doc, '$.status') = 'shipped';
SELECT o.id, i.value ->> '$.sku' FROM orders o, json_each(o.doc, '$.items') i;
UPDATE orders SET doc = json_set(doc, '$.status', 'delivered');
UPDATE orders SET doc = json_remove(doc, '$.coupon');
SELECT json_valid(doc), json_type(doc, '$.total'), json_array_length(doc, '$.items') FROM orders;
SELECT json_group_array(json_object('id', id, 'status', doc ->> '$.status')) FROM orders;
CREATE INDEX orders_status ON orders (json_extract(doc, '$.status')); -- expression index
SELECT jsonb(doc) FROM orders; -- binary form, 3.45+
SQL Server
SELECT JSON_VALUE(doc, '$.customer.name'), JSON_QUERY(doc, '$.items') FROM orders; -- scalar vs object/array
SELECT * FROM orders WHERE JSON_VALUE(doc, '$.status') = 'shipped';
SELECT * FROM orders CROSS APPLY OPENJSON(doc, '$.items') WITH (sku NVARCHAR(20) '$.sku', qty INT '$.qty');
UPDATE orders SET doc = JSON_MODIFY(doc, '$.status', 'delivered');
SELECT id, status FROM products FOR JSON PATH; -- rows to JSON
SELECT id, status FROM products FOR JSON PATH, ROOT('data');
ALTER TABLE orders ADD status AS JSON_VALUE(doc, '$.status'); CREATE INDEX ix ON orders(status);
-- SQL Server 2025 adds a native json type and JSON indexes
MongoDB and Redis
MongoDB: documents, queries, Extended JSON
db.orders.insertOne({_id: 10042, status: "shipped", customer: {name: "Ana"}, items: [{sku: "OAK-1", qty: 1}]})
db.orders.find({status: "shipped", "customer.name": "Ana"})
db.orders.find({"items.sku": "OAK-1"}, {status: 1, "items.$": 1}) // projection
db.orders.find({items: {$elemMatch: {sku: "OAK-1", qty: {$gt: 0}}}})
db.orders.updateOne({_id: 10042}, {$set: {status: "delivered"}, $push: {items: {sku: "RUG-9", qty: 1}}, $unset: {coupon: ""}})
db.orders.aggregate([{$unwind: "$items"}, {$group: {_id: "$items.sku", qty: {$sum: "$items.qty"}}}])
db.orders.createIndex({status: 1, "customer.name": 1})
// Extended JSON, as mongoexport writes it
{"_id": {"$numberLong": "10042"}, "created": {"$date": "2026-09-21T09:04:11Z"}, "ref": {"$oid": "66f0a1b2c3d4e5f6a7b8c9d0"}}
mongoexport --collection=orders --jsonArray --out orders.json mongoimport --jsonArray orders.json
Redis JSON and other stores
JSON.SET order:10042 $ '{"status": "shipped", "items": [{"sku": "OAK-1", "qty": 1}]}'
JSON.GET order:10042 $.status # ["shipped"]
JSON.SET order:10042 $.status '"delivered"'
JSON.NUMINCRBY order:10042 $.items[0].qty 1
JSON.ARRAPPEND order:10042 $.items '{"sku": "RUG-9", "qty": 1}'
FT.CREATE idx ON JSON PREFIX 1 order: SCHEMA $.status AS status TAG # query with RediSearch
# without the module: SET order:10042 '{...}' and parse in the app
# DynamoDB, Firestore, CouchDB, Cosmos DB: document stores that speak JSON at the API and store their own encoding
DuckDB, Warehouses, ORMs
DuckDB: SQL over JSON files, no server
duckdb -c "SELECT id, name, price FROM read_json_auto('products.json') WHERE price > 20"
duckdb -c "SELECT level, count(*) FROM read_json_auto('app.jsonl') GROUP BY 1" -- JSON Lines too
duckdb -c "SELECT o.id, i.sku, i.qty FROM read_json_auto('orders.json') o, UNNEST(o.items) AS t(i)" -- explode an array
duckdb -c "SELECT doc->'$.customer.name' AS name, json_extract_string(doc, '$.status') FROM read_json_objects('orders.json') t(doc)"
duckdb -c "COPY (SELECT * FROM read_json_auto('orders.jsonl')) TO 'orders.parquet'" -- convert
duckdb -c "COPY (SELECT * FROM products) TO 'products.json' (FORMAT JSON, ARRAY true)" -- export
# Python: import duckdb; duckdb.sql("SELECT * FROM 'products.json'").df()
BigQuery, Snowflake, Oracle
-- BigQuery: JSON type, dot access, JSON_VALUE / JSON_QUERY, load JSON Lines from GCS
SELECT JSON_VALUE(doc, '$.customer.name'), doc.items[0].sku, JSON_QUERY_ARRAY(doc.items) FROM orders;
SELECT * FROM orders, UNNEST(JSON_QUERY_ARRAY(doc.items)) AS item WHERE JSON_VALUE(item, '$.sku') = 'OAK-1';
bq load --source_format=NEWLINE_DELIMITED_JSON ds.orders gs://bucket/orders.jsonl
-- Snowflake: VARIANT, colon and dot access, FLATTEN for arrays, PARSE_JSON to load
SELECT doc:customer.name::string, doc:items[0].sku FROM orders;
SELECT o.id, f.value:sku::string FROM orders o, LATERAL FLATTEN(input => o.doc:items) f;
INSERT INTO orders SELECT PARSE_JSON('{"status": "shipped"}'); COPY INTO orders FROM @stage FILE_FORMAT = (TYPE = JSON);
-- Oracle 21c+: native JSON type, dot notation, JSON_TABLE, JSON_VALUE
SELECT o.doc.customer.name, JSON_VALUE(o.doc, '$.status') FROM orders o;
SELECT j.* FROM orders, JSON_TABLE(doc, '$.items[*]' COLUMNS (sku VARCHAR2(20) PATH '$.sku', qty NUMBER PATH '$.qty')) j;
ORMs: Django, Prisma, Eloquent, SQLAlchemy, Entity Framework
# Django
meta = models.JSONField(default=dict, blank=True)
Product.objects.filter(meta__specs__depth__gt=60) .filter(meta__has_key="finish") .filter(meta__contains={"color": "oak"})
// Prisma (PostgreSQL)
meta Json @default("{}")
prisma.product.findMany({where: {meta: {path: ["specs", "depth"], gt: 60}}})
// Eloquent
protected $casts = ['meta' => 'array']; // or AsArrayObject::class, AsCollection::class
Product::where('meta->specs->depth', '>', 60)->get(); Product::whereJsonContains('meta->tags', 'oak')->get();
# SQLAlchemy
meta = mapped_column(JSONB, default=dict) # MutableDict.as_mutable(JSONB) to track in place changes
session.query(Product).filter(Product.meta["specs"]["depth"].astext.cast(Integer) > 60)
// Entity Framework Core 7+: owned JSON columns
modelBuilder.Entity<Product>().OwnsOne(p => p.Meta, b => b.ToJson());
db.Products.Where(p => p.Meta.Specs.Depth > 60) // translated to JSON_VALUE
# in place edits: product.meta["specs"]["depth"] = 70 then save() works in Django (it reassigns), not in SQLAlchemy without MutableDict
Config Files & Conventions
the JSON files every project has, and a style guide
Files You Will Edit
package.json
{
"name": "shop-api",
"version": "2.4.0",
"private": true,
"type": "module",
"main": "./dist/index.js",
"exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } },
"engines": { "node": ">=20" },
"scripts": { "dev": "node --watch src/index.js", "test": "vitest", "build": "tsc" },
"dependencies": { "express": "^5.1.0" },
"devDependencies": { "typescript": "^5.6.0", "vitest": "^2.1.0" }
}
npm pkg get version npm pkg set scripts.lint="eslint ." npm pkg delete scripts.old
tsconfig.json and .vscode files are JSONC
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext",
"strict": true, "resolveJsonModule": true, "outDir": "dist", // comments are fine here
},
"include": ["src"],
}
// .vscode/launch.json, settings.json, extensions.json, tasks.json: same dialect
// .devcontainer/devcontainer.json, deno.jsonc, .hintrc: same
The rest of the usual suspects
composer.json PHP: {"require": {"php": "^8.3", "laravel/framework": "^12.0"}, "autoload": {"psr-4": {"App\\": "app/"}}}
appsettings.json .NET: {"Logging": {"LogLevel": {"Default": "Information"}}, "ConnectionStrings": {"Default": "..."}}
manifest.json browser extensions and PWAs: {"manifest_version": 3, "name": "...", "permissions": ["storage"]}
.eslintrc.json legacy ESLint config (flat config eslint.config.js replaced it); .prettierrc is JSON or YAML
renovate.json, .releaserc.json, lerna.json, nx.json, turbo.json, vercel.json, netlify.toml (not JSON), firebase.json
angular.json, nest-cli.json, .swcrc, .babelrc (JSON5), jsconfig.json (JSONC)
Cargo.toml, pyproject.toml, go.mod: not JSON, and that is fine
Editor help for any JSON file: $schema and SchemaStore
{ "$schema": "https://json.schemastore.org/package.json", ... }
{ "$schema": "https://json.schemastore.org/tsconfig", ... }
{ "$schema": "https://json.schemastore.org/github-workflow.json", ... } # for YAML files use the yaml.schemas setting
{ "$schema": "./config.schema.json", "port": 8080 } # your own
# catalogue: https://www.schemastore.org/json/ (hundreds of known files, matched by name automatically in VS Code and JetBrains)
Style Guide
Formatting rules that keep diffs small
indent 2 spaces (npm, Prettier, most JavaScript), 4 spaces (Python json.tool, PHP JSON_PRETTY_PRINT): pick one per repo
line endings LF; one newline at the end of the file (npm and Prettier add it, json.tool does not)
key order stable and meaningful: id first, then names, then the rest; or sorted when the file is generated
one key per line in config files; compact arrays of scalars on one line: "tags": ["a", "b"]
no trailing whitespace; no tabs inside strings (use \t)
encoding UTF-8 without BOM, always
# .editorconfig
[*.json]
indent_style = space
indent_size = 2
insert_final_newline = true
# .prettierrc: {"tabWidth": 2}; Prettier formats .json as JSON and tsconfig/.vscode as JSONC automatically
Secrets and environments
// wrong: config.json in git
{"database": {"password": "hunter2"}}
// right: the file names the setting, the environment supplies the value
{"database": {"url_env": "DATABASE_URL"}}
// or a template with placeholders filled at deploy time
{"database": {"url": "${DATABASE_URL}"}} // envsubst < config.template.json > config.json
// .NET layering: appsettings.json + appsettings.Production.json + environment variables + user secrets
// Node: config.json for defaults, process.env for overrides; dotenv for local development only
// never: JSON.parse(process.env.CONFIG) with a secret pasted into a CI variable that is echoed in logs
Versioning and validating config in CI
{"$schema": "./config.schema.json", "schema_version": 3, ...} # a version field lets a loader migrate old files
# CI: every JSON file must parse, every config must match its schema
find . -name "*.json" -not -path "./node_modules/*" -print0 | xargs -0 -n1 jq empty
check-jsonschema --schemafile config.schema.json config/*.json
npx prettier --check "**/*.json"
# pre-commit
- repo: https://github.com/pre-commit/pre-commit-hooks
hooks: [{id: check-json}, {id: pretty-format-json, args: [--autofix, --indent, "2"]}]
Loading Config Correctly
File, then environment, then validate, then freeze
import {readFileSync} from "node:fs";
import Ajv from "ajv";
const file = JSON.parse(readFileSync(process.env.CONFIG ?? "config.json", "utf8"));
const merged = {...file, port: process.env.PORT ? Number(process.env.PORT) : file.port}; // env wins
const validate = new Ajv({useDefaults: true, coerceTypes: true}).compile(schema); // defaults from the schema
if (!validate(merged)) { console.error("config error:", validate.errors.map((e) => `${e.instancePath} ${e.message}`).join("; ")); process.exit(1); }
export const config = Object.freeze(merged);
# Python: pydantic-settings reads the file and the environment in one model
class Settings(BaseSettings):
port: int = 8080
database_url: str
model_config = SettingsConfigDict(env_file=".env", json_file="config.json")
settings = Settings() # raises with the field name on a bad value
Edit JSON files from scripts without wrecking the diff
jq '.port = 9090' config.json > tmp && mv tmp config.json # jq rewrites the whole file: 2 space indent, key order kept
jq --indent 4 '.version = "2.5.0"' package.json # match the file's indent
npm pkg set version=2.5.0 # npm keeps package.json's own formatting
node -e 'const f="tsconfig.json";const {parse,modify,applyEdits}=require("jsonc-parser");const t=require("fs").readFileSync(f,"utf8");require("fs").writeFileSync(f,applyEdits(t,modify(t,["compilerOptions","strict"],true,{})))' # keeps comments
python -c 'import json,sys; p="config.json"; d=json.load(open(p)); d["port"]=9090; json.dump(d, open(p,"w"), indent=2); open(p,"a").write("\n")'
sed -i 's/"port": 8080/"port": 9090/' config.json # fine for one literal, fragile for anything else
# commit the formatter with the change: prettier --write config.json, and the diff stays one line
Structured Output & AI
getting valid JSON out of a language model, and checking it
Ask for a Schema, Not for JSON
OpenAI: response_format with a JSON Schema
from openai import OpenAI
client = OpenAI()
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"price": {"type": "number"},
"tags": {"type": "array", "items": {"type": "string"}},
"in_stock": {"type": "boolean"}
},
"required": ["name", "price", "tags", "in_stock"],
"additionalProperties": False
}
r = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[{"role": "user", "content": "Extract the product from: Oak desk, 199 EUR, in stock, tags oak and desk"}],
response_format={"type": "json_schema", "json_schema": {"name": "product", "schema": schema, "strict": True}},
)
product = json.loads(r.choices[0].message.content)
# strict mode rules: every property required, additionalProperties false, no format/pattern/minimum; optional = type ["string", "null"]
Claude: output_config.format with the same schema
import anthropic, json
client = anthropic.Anthropic()
r = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{"role": "user", "content": "Extract the product from: Oak desk, 199 EUR, in stock, tags oak and desk"}],
output_config={"format": {"type": "json_schema", "schema": schema}}, # the same JSON Schema as above
)
product = json.loads(next(b.text for b in r.content if b.type == "text")) # guaranteed valid and matching
# the same guarantee on tool arguments: strict tools
tools=[{"name": "record_product", "description": "Record the extracted product.", "strict": True, "input_schema": schema}]
# Gemini
model.generate_content(prompt, generation_config={"response_mime_type": "application/json", "response_schema": schema})
Skip the hand written schema: pydantic and Zod
# Python, OpenAI helper: the schema comes from the class, the reply comes back parsed
class Product(BaseModel):
name: str
price: float
tags: list[str]
in_stock: bool
r = client.beta.chat.completions.parse(model="gpt-4o-2024-08-06", messages=msgs, response_format=Product)
product: Product = r.choices[0].message.parsed
# Python, Anthropic SDK: parse() validates the reply against the model
r = anthropic.Anthropic().messages.parse(model="claude-opus-5", max_tokens=16000, messages=msgs, output_format=Product)
product: Product = r.parsed_output
# Python, any provider: pip install instructor
client = instructor.from_anthropic(anthropic.Anthropic())
product = client.messages.create(model="claude-opus-5", max_tokens=16000, messages=msgs, response_model=Product)
// TypeScript, Zod
import {zodResponseFormat} from "openai/helpers/zod";
const Product = z.object({name: z.string(), price: z.number(), tags: z.array(z.string()), in_stock: z.boolean()});
const r = await client.beta.chat.completions.parse({model, messages, response_format: zodResponseFormat(Product, "product")});
const product = r.choices[0].message.parsed; // typed, validated
// TypeScript, Anthropic SDK
import {zodOutputFormat} from "@anthropic-ai/sdk/helpers/zod";
const m = await anthropic.messages.parse({model: "claude-opus-5", max_tokens: 16000, messages, output_config: {format: zodOutputFormat(Product)}});
const product2 = m.parsed_output;
// Vercel AI SDK: generateObject({model, schema: Product, prompt})
When You Only Have a Prompt
A prompt that gets clean JSON most of the time
Return only a JSON object, no prose, no code fence, no comments.
Use exactly these keys: name (string), price (number), tags (array of strings), in_stock (boolean).
Use null for a value you cannot find. Do not add keys.
Example: {"name": "Lamp", "price": 25.25, "tags": ["light"], "in_stock": false}
# helps: an example, lowercase true/false/null, "no trailing commas", asking for the object alone
# still fails sometimes: fences, comments, single quotes, NaN, truncated output at max_tokens
Strip the fence, parse, repair, validate, retry
import json, re
from json_repair import repair_json # pip install json-repair
def extract_json(text):
m = re.search(r"```(?:json)?\s*(.*?)```", text, re.S) # 1. take the fenced block if there is one
candidate = m.group(1) if m else text
start, end = candidate.find("{"), candidate.rfind("}") # 2. or the outermost braces
if start != -1 and end != -1: candidate = candidate[start:end + 1]
try:
return json.loads(candidate) # 3. strict first
except json.JSONDecodeError:
return json.loads(repair_json(candidate)) # 4. then repair
data = extract_json(reply)
jsonschema.validate(data, schema) # 5. always validate; on failure retry with the error text
// JavaScript: npm i jsonrepair; JSON.parse(jsonrepair(text))
Streaming, tool calls, and keeping the model honest
# streaming structured output: parse the growing prefix
pip install partial-json-parser; from partial_json_parser import loads; loads('{"name": "Oak de') # {'name': 'Oak de'}
# OpenAI: client.beta.chat.completions.stream(..., response_format=Product) yields .parsed snapshots
# Anthropic: client.messages.stream(...) with output_config.format streams the JSON text; input_json_delta events carry partial tool arguments
# tool calling is structured output in the other direction: the model emits a tool call such as {"name": "get_price", "input": {"sku": "OAK-1"}}
# validate arguments against the tool's schema before executing anything; never eval; treat sku as untrusted input
# evaluation: log every reply, count parse failures and schema failures per model version, and keep the failing samples as tests
# cost: schemas count as input tokens; strict mode adds latency on the first call while the grammar is compiled, then it is cached
Security
what can go wrong with untrusted JSON, and the fix for each
Parsing Untrusted Input
Prototype pollution: not the parser, the merge after it
// dangerous pattern
function merge(target, src) { for (const k in src) { target[k] = typeof src[k] === "object" ? merge(target[k] ?? {}, src[k]) : src[k]; } return target; }
merge(config, JSON.parse(body)); // body: {"__proto__": {"isAdmin": true}}
// fixes
const BAD = new Set(["__proto__", "constructor", "prototype"]);
if (BAD.has(k)) continue; // in your own merge
const obj = Object.create(null); // objects with no prototype to pollute
Object.freeze(Object.prototype); // at startup, in Node services
npm audit // lodash < 4.17.21, minimist, and others had this bug
// validate first: Ajv with additionalProperties: false rejects unknown keys before any merge
Denial of service: size and depth limits
express.json({limit: "100kb"}) // Express default is 100kb; keep it small
app.use(bodyParser.json({limit: "1mb", strict: true})) // strict: only objects and arrays at the top
client_max_body_size 1m; # nginx
DATA_UPLOAD_MAX_MEMORY_SIZE = 2_621_440 # Django
json_decode($body, true, 64, JSON_THROW_ON_ERROR) // PHP depth 64 instead of 512
new JsonSerializerOptions { MaxDepth = 32 } // .NET, default 64
JsonMapper.builder().streamReadConstraints(StreamReadConstraints.builder().maxNestingDepth(100).maxStringLength(1_000_000).build()) // Jackson 2.15+
# Python's json has no size limit; check len(body) before json.loads, and RecursionError covers depth
# reject Content-Length above your limit before reading the body at all
Duplicate keys and parser differences
{"amount": 1, "amount": 1000} JavaScript, Python, Go, PHP, Ruby: 1000 (last wins); some Java configs: first; jq: last
# detect duplicates
python: json.loads(text, object_pairs_hook=lambda pairs: reject_dupes(pairs))
node: npm i json-parse-even-better-errors is not enough; use a streaming parser (clarinet) or a schema that reads the raw text
Go: encoding/json/v2 rejects duplicates; v1 keeps the last
.NET: System.Text.Json keeps the last; Newtonsoft keeps the last
# also disagree: large numbers (rounded vs exact), \u0000 in keys, invalid UTF-8, a BOM, whitespace like U+00A0, trailing garbage
Output and Transport
Embedding JSON in HTML safely
// Node / templates
const safe = JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
<script>window.__DATA__ = ${safe};</script>
// framework helpers do the same
Django {{ data|json_script:"app-data" }} then JSON.parse(document.getElementById("app-data").textContent)
Rails <%= raw data.to_json %> is unsafe; use json_escape or <%= data.to_json.html_safe %> only with the escaping ActiveSupport does by default
PHP json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT)
Laravel @json($data) or Js::from($data)
// a <script type="application/json"> block still needs the < escaping; it only stops execution, not tag closing
Never eval it, and set the right headers
eval("(" + text + ")") // runs code inside the payload; JSON.parse is the only parser
new Function("return " + text) // same thing
$.getJSON / JSONP // JSONP is executable JavaScript from a third party; use CORS + fetch instead
Content-Type: application/json // never text/html for a JSON body; browsers may render it and run nothing, but XSS scanners and proxies behave differently
X-Content-Type-Options: nosniff // stops MIME sniffing of the response
Content-Disposition: attachment // when the JSON is a user download
// GET endpoints that return sensitive JSON arrays: require a header or POST, or prefix ")]}',\n" as Google does, to defeat the old JSON hijacking trick
Mass assignment and over-posting
// body: {"name": "Ana", "role": "admin", "balance": 999999}
User.update(req.body) // writes role and balance too
// fixes: allow list the fields
const {name, email} = req.body; User.update({name, email});
Product.parse(body) // a zod schema with .strict() rejects extra keys
$request->validated() // Laravel: only validated fields
serializer.validated_data // DRF; Django ModelForm fields = [...]
[Bind("Name,Email")] / a DTO class // ASP.NET Core
params.require(:user).permit(:name, :email) // Rails strong parameters
Deserialisation gadgets, JWTs, and signing
# polymorphic deserialisation: never let the payload pick the class
Jackson enableDefaultTyping / @JsonTypeInfo(use = CLASS) with untrusted input = remote code execution; use NAME with a fixed @JsonSubTypes list
.NET TypeNameHandling.All in Json.NET is the same bug; System.Text.Json has no equivalent, by design
Python json is safe; pickle and yaml.load are not; yaml.safe_load only
PHP json_decode is safe; unserialize() is not
# JWT: the payload is JSON, readable by anyone; verify the signature before trusting a single field
header {"alg": "HS256", "typ": "JWT"} reject "alg": "none"; pin the algorithm on the server, never read it from the token
payload {"sub": "12", "exp": 1789981451} check exp, iss, aud
# sign or MAC JSON bodies for webhooks over the raw bytes, not over a re-serialised copy (key order and whitespace change)
URLs and Logs
JSON in query strings
const filter = JSON.parse(new URL(req.url).searchParams.get("filter") ?? "{}"); // encodeURIComponent on the way out
const Filter = z.object({status: z.enum(["draft", "live"]), category: z.string().max(40)}).strict();
const safe = Filter.parse(filter); // rejects $ne, $where, extra keys
Product.find(safe); // now a plain object
# limits: keep URLs under ~2000 characters; a JSON filter that grows past that belongs in a POST body (or a saved search)
# express: app.set("query parser", "simple") to stop qs from building nested objects out of a[b]=c
# never log full URLs that carry tokens or personal data in a JSON parameter
Redaction in JSON logs
// pino: redact by path at the logger
const log = pino({redact: {paths: ["password", "*.password", "req.headers.authorization", "user.email", "card.number"], censor: "[REDACTED]"}});
log.info({user, password: "x"}, "login");
# Python structlog / logging: a processor that walks the dict
SENSITIVE = {"password", "token", "authorization", "secret", "card", "ssn"}
def redact(_, __, event):
for k in list(event):
if k.lower() in SENSITIVE: event[k] = "[REDACTED]"
elif isinstance(event[k], dict): redact(_, __, event[k])
return event
# Django: DEFAULT_EXCEPTION_REPORTER_FILTER hides settings named *SECRET*, *PASSWORD*, *KEY* in error mails
# Rails: config.filter_parameters += [:password, :token, :card]
# Sentry: send_default_pii = False; before_send scrubs; EventScrubber for custom keys
# retention: JSON logs with personal data fall under GDPR; set a TTL on the index
Performance & Big Files
streaming, compression, faster parsers, binary cousins
Streaming Instead of Loading
Python: ijson yields items from a top level array
pip install ijson
import ijson
with open("orders.json", "rb") as f:
for order in ijson.items(f, "item"): # "item" = each element of the top level array
process(order)
for sku in ijson.items(f, "orders.item.items.item.sku"): # a nested path
...
# ijson picks the C backend (yajl) when available; pure Python is 10x slower
# alternatives: json-stream, jsonslicer; or convert to JSON Lines once and stream lines forever
Node: stream-json
npm i stream-json
import {createReadStream} from "node:fs";
import {parser} from "stream-json";
import {streamArray} from "stream-json/streamers/StreamArray.js";
import {pick} from "stream-json/filters/Pick.js";
createReadStream("orders.json")
.pipe(parser())
.pipe(pick({filter: "orders"})) // descend into the "orders" key
.pipe(streamArray())
.on("data", ({value}) => process(value))
.on("end", () => console.log("done"));
// fetch: process a JSON Lines response as it arrives
for await (const chunk of res.body.pipeThrough(new TextDecoderStream())) { for (const line of chunk.split("\n")) { if (line) handle(JSON.parse(line)); } }
Java, Go, .NET, and the command line
// Java: Jackson streaming API (JsonParser), or Gson JsonReader
try (JsonParser p = factory.createParser(file)) { while (p.nextToken() != null) { ... } }
// Go: json.Decoder over a reader, one value at a time
dec := json.NewDecoder(f); dec.Token() // consume "["
for dec.More() { var o Order; dec.Decode(&o) }
// .NET: Utf8JsonReader over a stream, or JsonSerializer.DeserializeAsyncEnumerable
await foreach (var o in JsonSerializer.DeserializeAsyncEnumerable<Order>(stream)) { ... }
# command line: jq --stream emits [path, leaf] pairs without loading the file
jq -c --stream 'select(length == 2)' big.json | head
jq -cn --stream 'fromstream(1 | truncate_stream(inputs))' big.json # top level array elements, one per line
Smaller and Faster
Compression beats everything else
# nginx
gzip on; gzip_types application/json application/x-ndjson; gzip_min_length 1024;
brotli on; brotli_types application/json;
# Express
app.use(compression());
# check a response
curl -s -H "Accept-Encoding: gzip, br" -o /dev/null -w "%{size_download}\n" https://api.example/products
gzip -k orders.json; brotli -k orders.json; zstd orders.json
Faster parsers when the parser is the bottleneck
Python orjson (Rust, fastest), msgspec (typed, very fast), ujson, rapidjson; json.loads is fine below ~1k docs/s JavaScript V8's JSON.parse is already SIMD assisted; simdjson bindings exist; avoid JSON.parse in a hot loop over tiny strings Java Jackson (fast), jsoniter, DSL-JSON; keep one ObjectMapper; use afterburner / blackbird modules Go encoding/json (ok), goccy/go-json, sonic (SIMD), json-iterator; encoding/json/v2 is faster .NET System.Text.Json with source generation; Utf8JsonReader for zero allocation scanning PHP ext-json is C and fast; ext-simdjson for huge inputs C/C++/Rust simdjson (2+ GB/s), yyjson, RapidJSON, serde_json with the "float_roundtrip" feature off for speed
Shrink the payload itself
omit nulls and defaults {"coupon": null} on every record adds up; send absent instead (document it)
shorter keys are not worth it {"n": ...} saves bytes only before compression and costs readability forever
sparse fields ?fields=id,name,price so the server sends only what the screen needs
pagination never return 50,000 records in one array
ids as numbers where safe "id": 12 not "id": "12", unless they exceed 2^53
avoid double encoding a JSON string containing JSON is parsed twice and escaped once
dates as ISO strings 24 bytes; an integer timestamp is 10, rarely worth the ambiguity
column oriented for big tables {"cols": ["id","price"], "rows": [[1,199],[2,25]]} halves repeated keys (then compress anyway)
Binary cousins, when JSON is not enough
MessagePack same data model as JSON, 20 to 50% smaller, faster; msgpack in every language; Redis, Fluentd use it CBOR RFC 8949, the IETF's MessagePack: tags, dates, big numbers; used in WebAuthn, COSE, IoT BSON MongoDB's format: more types, NOT smaller than JSON Protocol Buffers schema first, tiny, typed, versioned; gRPC; needs .proto files and generated code Avro schema travels with the data, great for Kafka and Hadoop Parquet columnar, compressed, for analytics; convert JSON Lines to Parquet for anything you query repeatedly FlatBuffers / Cap'n Proto zero copy reads, games and low latency # a quick comparison for the same record JSON 118 bytes MessagePack 86 bytes CBOR 85 bytes Protobuf 41 bytes JSON+gzip 96 bytes (tiny records do not compress well)
Errors & Debugging
the messages, what they mean, and where to look
Error Messages Decoded
| Message | Where | Usual cause | Fix |
|---|---|---|---|
| Unexpected token < in JSON at position 0 | JavaScript | the server sent HTML: a 404, a login page, an error page | check res.ok and res.headers.get("content-type") before res.json() |
| Unexpected token u in JSON at position 0 | JavaScript | you parsed the string "undefined": the variable was never set, or localStorage key is missing | guard with ?? "null" or check for null first |
| Unexpected end of JSON input | JavaScript | empty body, truncated response, unbalanced brackets | read res.text() and look; check Content-Length and timeouts |
| Unexpected token ' in JSON | JavaScript | single quoted strings or keys | double quotes |
| Unexpected token } / ] in JSON | JavaScript | trailing comma before the closing bracket | remove the comma |
| Bad control character in string literal | JavaScript | a raw newline or tab inside a string | escape as \n / \t |
| Converting circular structure to JSON | JavaScript | an object references itself (DOM nodes, ORM models) | a replacer, toJSON, or serialise a plain projection |
| Do not know how to serialize a BigInt | JavaScript | a BigInt in the data | replacer to string, or JSON.rawJSON |
| Expecting property name enclosed in double quotes | Python | single quotes, unquoted keys, or a trailing comma in an object | fix the source; if it is Python repr, use ast.literal_eval not json |
| Expecting value: line 1 column 1 (char 0) | Python | empty string, HTML, or a BOM at the start | print repr(text[:80]); open with encoding="utf-8-sig" for BOM files |
| Object of type datetime is not JSON serializable | Python | a date, Decimal, UUID, set or dataclass in the data | default=str or a proper default function |
| Extra data: line 2 column 1 | Python | two documents in one string, usually JSON Lines | parse per line, or wrap in an array |
| Syntax error (4) / json_decode returns null | PHP | invalid JSON, or valid JSON null | JSON_THROW_ON_ERROR; json_last_error_msg() |
| Malformed UTF-8 characters (5) | PHP | Latin-1 bytes from a database or file | mb_convert_encoding($s, 'UTF-8', 'ISO-8859-1') or JSON_INVALID_UTF8_SUBSTITUTE |
| Unrecognized field "x" not marked as ignorable | Jackson | the API added a field | FAIL_ON_UNKNOWN_PROPERTIES = false or @JsonIgnoreProperties(ignoreUnknown = true) |
| Cannot deserialize value of type X from String | Jackson | a number sent as a string, or a date in an unexpected format | coercion config, @JsonFormat, or fix the producer |
| The JSON value could not be converted to X | .NET | type mismatch, or camelCase vs PascalCase with case sensitive matching | JsonSerializerDefaults.Web, NumberHandling.AllowReadingFromString |
| json: cannot unmarshal string into Go value of type int | Go | type mismatch | match the struct type, or json.Number / a custom UnmarshalJSON |
| invalid character 'x' looking for beginning of value | Go | not JSON at all, often HTML or a BOM | log the first bytes of the body |
| parse error: Invalid numeric literal at line 1, column 8 | jq | unquoted string or key, single quotes, or the input is not JSON (YAML, text) | jq reads plain JSON only; -R for raw lines |
Debugging Checklist
Look at the bytes before you blame the parser
// JavaScript
const text = await res.text(); console.log(res.status, res.headers.get("content-type"), JSON.stringify(text.slice(0, 200)));
const data = JSON.parse(text);
# Python
print(r.status_code, r.headers.get("content-type"), repr(r.text[:200]))
# shell
curl -si https://api.example/x | head -30 # status + headers + start of body
curl -s https://api.example/x | head -c 300 | xxd # bytes: look for EF BB BF (BOM) or 3C 68 74 6D 6C (<html)
file body.json # "JSON text data" vs "HTML document" vs "UTF-8 Unicode (with BOM) text"
Encoding problems: BOM, Latin-1, mojibake
cat -A file.json | head # M-oM-;M-? at the start is a BOM; ^I is a raw tab; M-CM-) is é in UTF-8
sed -i '1s/^\xEF\xBB\xBF//' file.json # strip the BOM
iconv -f ISO-8859-1 -t UTF-8 old.json > new.json # convert Latin-1
python: open(path, encoding="utf-8-sig") # reads and drops a BOM
python: text.encode("latin-1").decode("utf-8") # undo mojibake like "Café" when it was double decoded
node: Buffer.from(bytes).toString("utf8").replace(/^\uFEFF/, "")
# save from editors as "UTF-8" not "UTF-8 with BOM"; Notepad and older Excel exports add it
Browser DevTools
Network tab click the request, Preview shows the parsed tree, Response shows the raw text, Headers shows Content-Type
filter: type "fetch/xhr"; right click, Copy as cURL to reproduce in a terminal; Copy response
Console copy(JSON.stringify(obj, null, 2)) puts formatted JSON on the clipboard
console.table(rows) a table for an array of objects
JSON.parse(text) run it directly on the copied response to see the exact error
Sources set a breakpoint on the line that calls JSON.parse, inspect the string in the scope panel
Application Local Storage and Session Storage values are shown raw; double click to edit
# Firefox renders JSON responses as a collapsible tree by default; Chrome needs a JSON viewer extension
Double encoding, and other shape bugs
{"payload": "{\"id\": 12}"} a JSON string containing JSON: someone called stringify twice or stored text in a JSON column
fix: JSON.parse(obj.payload) once, then stop encoding at the source
{"tags": "[\"a\", \"b\"]"} same bug on an array
{"count": "3", "active": "true"} stringly typed: a form serialiser or a spreadsheet export; convert at the boundary with a schema
{"date": "2026-09-21T09:04:11.000Z"} vs {"date": 1789981451} two producers, two conventions; normalise in one place
{"user": {"id": 12}} vs {"user_id": 12} nested vs flat: pick one shape per resource
[] vs null vs missing three different "empty" states; document which one each field uses
When Valid JSON Still Breaks the App
Shape drift, and guarding against it once
order.customer.name // TypeError when customer is null; use order.customer?.name ?? "unknown" items.map(...) // TypeError when items is an object because the converter collapsed a single element: [].concat(items) price * 3 // "199" * 3 is 597 by luck; "19,99" * 3 is NaN: Number(price) with a check, or a schema with coerceTypes new Date(created) // works for ISO, gives Invalid Date for "21.09.2026": parse with a known format JSON.parse(res) // res was already an object (axios): typeof res === "string" ? JSON.parse(res) : res for (const k in data) // inherited keys when something polluted the prototype: Object.keys(data) data.length // undefined on an object: Object.keys(data).length
Find the odd records fast
jq '[.[] | .price | type] | unique' orders.json # which types does a field have across records
jq 'map(select(.customer == null)) | length' orders.json # how many records miss it
jq '[.[] | keys] | add | unique' orders.json # every key that appears anywhere
jq 'group_by(keys) | map({keys: .[0] | keys, n: length})' orders.json # the distinct shapes and how common each is
jq '.[] | select(.created | test("^\\d{4}-\\d{2}-\\d{2}T") | not) | .id' orders.json # dates not in ISO form
python: pd.json_normalize(rows).dtypes; df.isna().sum(); df[df.price.map(type) != float]
JSON vs YAML, XML & More
when each format wins, the spec history, a glossary
Format Comparison
| Format | Comments | Schema | Types | Human editing | Size | Best for |
|---|---|---|---|---|---|---|
| JSON | no | JSON Schema | 6, no dates | fair | medium | APIs, interchange, storage, anything a machine writes |
| JSON5 / JSONC | yes | JSON Schema | JSON + Infinity, NaN | good | medium | config files you control the parser for |
| JSON Lines | no | per line | JSON | fair | medium | logs, exports, streams, datasets |
| YAML | yes | JSON Schema (via tools) | many, guessed | good until it is not | small | CI pipelines, Kubernetes, Docker Compose, human edited config |
| TOML | yes | none standard | explicit, dates | good for flat config | medium | Cargo, pyproject, app settings |
| XML | yes | XSD, RelaxNG | text + schema | verbose | large | documents, SOAP, legacy enterprise, mixed content |
| CSV | no | none | text only | in a spreadsheet | small | flat tables, Excel, imports |
| Protobuf | in .proto | required | rich, typed | no (binary) | tiny | gRPC, internal services, mobile |
| MessagePack / CBOR | no | none | JSON + binary, dates | no (binary) | small | drop in binary JSON |
| Parquet | no | in the file | columnar, typed | no | tiny | analytics, data lakes |
The same record four ways
JSON {"id": 12, "name": "Oak desk", "tags": ["oak", "desk"], "price": 199}
YAML id: 12
name: Oak desk
tags: [oak, desk]
price: 199
XML <product id="12"><name>Oak desk</name><tags><tag>oak</tag><tag>desk</tag></tags><price>199</price></product>
CSV id,name,tags,price
12,Oak desk,"oak,desk",199 (the array does not fit; CSV is flat)
Converting Between Them
YAML and TOML
yq -o json config.yaml yq -p json -o yaml data.json # mikefarah yq python -c 'import sys, yaml, json; print(json.dumps(yaml.safe_load(sys.stdin), indent=2))' < config.yaml python -c 'import sys, tomllib, json; print(json.dumps(tomllib.load(sys.stdin.buffer), indent=2))' < pyproject.toml # 3.11+ npx js-yaml config.yaml npx @iarna/toml config.toml # every valid JSON document is valid YAML 1.2; the reverse is not true
CSV
mlr --icsv --ojson cat products.csv # Miller; add --infer-none / -S to keep strings
python -c 'import csv, json, sys; print(json.dumps(list(csv.DictReader(sys.stdin)), indent=2))' < products.csv
npx csvtojson products.csv
jq -r '(.[0] | keys_unsorted) as $k | $k, (.[] | [.[$k[]]]) | @csv' products.json > products.csv # JSON to CSV
jq -r '.[] | [.id, .name, (.tags | join(";"))] | @csv' # flatten an array
pandas: pd.read_csv("p.csv").to_json(orient="records") pd.json_normalize(records).to_csv("p.csv", index=False)
XML
yq -p xml -o json data.xml # attributes become "+@id", text becomes "+content" python: pip install xmltodict; xmltodict.parse(xml) xmltodict.unparse(obj) node: npm i fast-xml-parser; new XMLParser().parse(xml) new XMLBuilder().build(obj) # there is no canonical mapping: attributes vs elements, repeated elements vs arrays, mixed content; expect to write rules
Binary
python: msgpack.packb(obj) msgpack.unpackb(b) cbor2.dumps(obj) bson.encode(obj)
node: npm i msgpackr / @msgpack/msgpack / cbor-x
protobuf: define .proto, then protoc; JSON mapping is standardised (protojson) so the same message can go either way
parquet: pandas / polars / duckdb: duckdb -c "COPY (SELECT * FROM read_json_auto('p.jsonl')) TO 'p.parquet'"
duckdb -c "SELECT * FROM read_json_auto('orders.json') WHERE total > 100" # SQL directly over JSON files
History and Glossary
| Year | Milestone | What changed |
|---|---|---|
| 2001 | Douglas Crockford names JSON | a subset of JavaScript object literals for State Software's data exchange |
| 2002 | json.org | the one page grammar and the railroad diagrams |
| 2005 | Ajax | JSON replaces XML in browser requests; the term Ajax is coined |
| 2006 | RFC 4627 | first informational RFC; application/json; top level must be object or array |
| 2009 | ES5 | JSON.parse and JSON.stringify become part of JavaScript; MongoDB and CouchDB store JSON |
| 2013 | ECMA-404 | Ecma standardises the syntax alone |
| 2014 | RFC 7159 / RFC 7493 | any value at the top level; I-JSON profile for interoperability (UTF-8, doubles, unique keys) |
| 2017 | RFC 8259 | current standard; UTF-8 required for open exchange; ISO/IEC 21778:2017 published the same grammar |
| 2018 | JSON5 1.0 | the human friendly superset gets a spec |
| 2020 | JSON Schema 2020-12 | current schema draft; adopted by OpenAPI 3.1 in 2021 |
| 2024 | RFC 9535 | JSONPath standardised after 17 years of dialects |
| 2024 | JSON.rawJSON, JSON.parse source access | ES2024 proposal shipped in V8: exact big numbers without quoting |
Glossary
document / text one complete JSON value, usually an object or array
member / key / name a "key": value pair inside an object; the spec says "name"
element one value inside an array
literal true, false, null
serialise / encode / stringify / dump value to text
parse / decode / load text to value
pretty print / format / indent add whitespace for humans; minify / compact: remove it
canonical JSON one byte exact form for hashing: sorted keys, no whitespace (RFC 8785, JCS)
superset a format that accepts every JSON document plus more: JSON5, JSONC, YAML 1.2
profile / subset a stricter JSON: I-JSON (RFC 7493), Canonical JSON
schema a JSON document that describes other JSON documents (JSON Schema)
pointer / path an address inside a document: /a/b/0 (JSON Pointer), $.a.b[0] (JSONPath), .a.b[0] (jq)
patch a description of changes: JSON Patch (operations), Merge Patch (an overlay)
envelope a wrapper object around the real payload: {"data": ..., "meta": ...}
NDJSON / JSON Lines one document per line; concatenated JSON: documents back to back without separators
BOM byte order mark, EF BB BF, must not start a JSON text
reviver / replacer JavaScript hooks that transform values while parsing / stringifying
JSONP the pre CORS hack of loading JSON as a script; obsolete and unsafe
What Is JSON?
JSON (JavaScript Object Notation) is a lightweight, text-based data interchange format built on key-value pairs and ordered lists. This JSON cheat sheet exists because the format is small enough to learn in an afternoon and fiddly enough that everyone still looks up the escapes, the number rules and the parser quirks.
It reads like plain text but maps directly onto the data structures most programming languages already use.
Key figures:
- JSON's syntax traces back to 2001, when it was first presented at the JSON.org website by Douglas Crockford, according to the ECMA-404 specification text
- The IETF standardized the grammar as RFC 8259 in December 2017, edited by Tim Bray
- Ecma International published the parallel ECMA-404 specification, 2nd edition, the same month
- The application/json media type registration was last updated December 15, 2017, according to IANA's own registry
JSON's role goes beyond syntax. It gives front-end development teams and server-side teams a shared data shape, so a browser and a database can agree on what a piece of information looks like without either side translating it first.
The same file format works for back-end development layers, configuration files like npm's package.json, and stored records, because the format doesn't care which language reads or writes it.
GitHub's REST API and most public web APIs return JSON by default, which is part of why the format spread as fast as it did.
Three traits explain the staying power:
- Human-readable text, easy to scan in an editor
- Machine-readable structure, easy to parse programmatically
- Minimal syntax, just braces, brackets, colons, and commas
JSON Syntax Rules
JSON syntax rests on four symbols: curly braces, square brackets, colons, and commas.
Get those four right, along with proper quoting, and the rest of the format falls into place.
- Key-value pairs: every entry is a quoted key, a colon, then a value
- Comma separation: commas divide entries inside both objects and arrays, with no trailing comma after the last one
- Double quotes: keys and string values require double quotes specifically, since single quotes break the format
- Whitespace handling: spaces, tabs, and line breaks between tokens are ignored by parsers
Pretty-printed JSON and minified JSON parse identically, because whitespace outside a string carries no meaning.
Character encoding follows UTF-8 by convention, and the grammar itself is defined over Unicode code points rather than raw bytes.
JSON Data Types
JSON supports exactly six data types, and nothing outside that list is valid JSON.
| Type | Example | Notes |
|---|---|---|
| String | "city": "London" | Always double-quoted |
| Number | "age": 45 | No leading zeros, no hex |
| Boolean | "active": true | Only true or false |
| Null | "middleName": null | Represents no value |
| Object | {"id": "210"} | Unordered key-value set |
| Array | [1, 2, 3] | Ordered, comma-separated |
Strings carry most of the formatting rules. Every string needs escape characters for special cases like a line break or an embedded quote.
- \n for a newline
- \" for an embedded double quote
- \u followed by four hex digits for a Unicode code point
Numbers skip the type variety most languages have. There's no separate integer type and no float type, just one number format that covers both.
JSON has no native date type, no comments, and no undefined value, all common sources of confusion for developers coming from JavaScript itself.
MongoDB's BSON format extends JSON's number type to support 64-bit integers and native dates, values plain JSON text cannot represent on its own.
JSON Objects vs Arrays
Objects and arrays are the two container types in JSON, and almost every real document combines both.
The difference comes down to whether data is keyed or ordered.
Objects
A JSON object is an unordered collection of key-value pairs wrapped in curly braces.
Each key is a double-quoted string, followed by a colon, followed by its value.
- Keys must be strings, always in double quotes
- Values can be any JSON data type, including another object
- Commas separate each pair, with no trailing comma after the last one
Object notation is the backbone of almost every JSON file you'll open.
Arrays
Ordered lists:
- Arrays hold values inside square brackets
- Order is preserved and matters for retrieval
- Values can mix types, though most real-world arrays stay uniform
A list of objects inside an array, often shown as a json array of objects, is the shape most RESTful API endpoints use when they return multiple records at once.
Nesting Objects and Arrays
- Object inside object: a value can itself be a full object, useful for hierarchical data like an address inside a user record
- Array inside object: a key's value can be an array, useful for lists like a user's tags or a product's sizes
Nesting has no fixed depth limit in the JSON grammar itself, though most parsers cap recursion to avoid stack overflows on deeply nested documents.
How to Parse JSON
Parsing turns JSON text into a native data structure the programming language can actually use.
Every mainstream language ships a built-in parser, so the process rarely needs a third-party library.
- JavaScript: JSON.parse() converts a JSON string into a JavaScript object and throws a SyntaxError on malformed input, a behavior documented on MDN Web Docs
- Python: json.loads() parses a JSON string, while json.load() reads directly from an open file
Both follow the same grammar, so a well-formed JSON string parses identically whether the runtime is Node.js or CPython.
When the source is a plain string variable rather than a file, a string to JSON converter can check and convert it without writing a script.
For the wider Python language beyond its json module, a python reference guide covers the rest of the syntax.
How to Serialize JSON
Serialization is the reverse of parsing. It turns a native object, dictionary, or list back into JSON text.
- JavaScript objects become JSON strings
- Python dictionaries become JSON strings
- Nested structures serialize recursively, layer by layer
JSON.stringify() handles this in JavaScript, and it accepts an optional indentation argument for pretty printing instead of a single minified line.
For a deeper look at the language's other methods, a javascript reference guide is worth keeping open while writing serialization code.
json.dumps() is the Python equivalent, and it takes the same kind of indent argument to control minification versus readable output.
When the object being serialized already exists as a Python dictionary, a python dict to JSON converter skips writing the conversion script entirely.
Serialized output should always pass through a validator before it's sent anywhere important, since a missing comma or an unescaped quote breaks the receiving parser.
JSON Validation Tools and Schema
A JSON document can look fine to the eye and still fail to parse, which is why validation exists as its own step.
The current stable release of JSON Schema, Draft 2020-12, was published in June 2022, according to the JSON Schema project's own specification page.
| Tool | Checks Syntax | Checks Schema | Best For |
|---|---|---|---|
| JSONLint | Yes | No | Quick syntax checks |
| JSON Schema | No | Yes | Structural and type validation |
| Visual Studio Code | Yes | Partial | Inline editing feedback |
Syntax validation only confirms the braces, brackets, and commas are in the right place.
Schema validation goes further. It confirms that a "price" field is actually a number and that a required "email" key hasn't been left out.
OpenAPI Specification documents commonly embed JSON Schema to define the request and response bodies for REST endpoints.
Tools like jq and JSONPath query and reshape JSON data rather than validate it, so they solve a different problem entirely.
JSON vs XML vs YAML
All three formats move structured data between systems, but they trade off readability, verbosity, and tooling differently.
JSON keeps syntax minimal. XML wraps everything in tags. YAML strips out punctuation almost entirely and relies on indentation.
| Format | Verbosity | Native Data Types | Common Use Case |
|---|---|---|---|
| JSON | Low | String, number, boolean, null, object, array | REST API payloads |
| XML | High | Text only, types inferred by schema | SOAP APIs, legacy enterprise systems |
| YAML | Low | Same core types as JSON, plus dates | Configuration and deployment files |
The governing specifications reflect the timeline. The W3C published XML 1.0's Fifth Edition on November 26, 2008, while the current YAML 1.2.2 specification dates to October 1, 2021, according to yaml.org.
Kubernetes configuration files are written in YAML, precisely because indentation-based syntax reads faster than nested XML tags at that scale.
For YAML's own syntax rules, indentation and anchors included, a yaml reference guide covers it in more depth.
XML still shows up where a strict schema and namespace system matter more than file size, particularly in older enterprise and financial messaging systems.
Teams migrating a config file between formats typically reach for a JSON to YAML converter instead of manually remapping every key by hand.
Common JSON Syntax Errors
Most invalid JSON fails for one of a handful of predictable reasons.
- Trailing commas: a comma left after the last item in an object or array, copied over from JavaScript object notation where it's allowed
- Unquoted or single-quoted keys: keys written as name instead of "name," or wrapped in 'single quotes' instead of double
- Missing closing brackets: an unmatched curly brace or square bracket, often buried deep in a nested structure
- Inline comments: // or /* */ left in a file, since standard JSON parsers reject any comment syntax
- Unescaped characters: a raw double quote or backslash inside a string value, breaking the string boundary
Every one of these produces a parsing error rather than a partial result. JSON parsers don't recover gracefully, and one bad character stops the entire document from loading.
Running the file through a linter catches all five in seconds, well before the file reaches production code.
How to Write Valid JSON
There's no single correct style, but there is a fixed sequence that produces valid output every time.
Skip a step and the document usually still looks fine to a human, right up until a parser rejects it.
- 1. Wrap the whole document in a single object or array
- 2. Quote every key with double quotes, never single
- 3. Assign each key one of the six valid data types
- 4. Separate entries with commas, and drop the comma after the last one
- 5. Run the file through a validator before saving or sending it
Editors like Visual Studio Code flag most of these issues inline, before the file is even saved.
Where JSON Is Used
Postman's 2024 State of the API Report found the average application depends on 26 to 50 APIs, most exchanging JSON, with 74% of developers calling themselves API-first, up from 66% in 2023.
- REST API request and response bodies
- Editor and tool configuration, such as Visual Studio Code's settings.json
- NoSQL document storage, where databases like MongoDB skip the object-relational mapping step entirely
- Browser-to-server data exchange behind most modern web apps
Most web apps exchange this data behind the scenes on every page load, well before a user notices anything happening.
JSON Web Token payloads are a quieter example. The token's body is a base64-encoded JSON object carrying claims like a user ID or an expiration time.
Pros and Cons of JSON
JSON's strengths and weaknesses both come from the same design choice: keep the format small and simple.
Pros:
- Lightweight, with no closing tags or namespace overhead
- Native support in JavaScript and near-universal libraries elsewhere
- Easy for a human to read and edit by hand
- Fast to parse, since the grammar is small and unambiguous
Cons:
- No comments, which hurts long-lived configuration files
- No native date type, forcing string workarounds
- No schema enforcement built into the format itself, unlike XML with DTDs
- Silent handling of duplicate keys, depending on the parser
Most of the cons have workarounds. None of the workarounds are part of the JSON grammar itself, which is exactly the tradeoff for staying this small.
When JSON Does Not Work
JSON breaks down in a handful of predictable situations, not because the format is flawed, but because it was never designed to solve these problems. No JSON cheat sheet fixes them, so it helps to recognise them early.
Large integers are the clearest case. JavaScript's Number type is only safe up to 9,007,199,254,740,991 (2^53 minus 1), and any JSON integer past that point gets silently corrupted when a parser coerces it, according to MDN Web Docs.
- Precision-critical IDs: 64-bit database identifiers or Snowflake-style IDs should travel as strings, not raw numbers
- Heavily annotated documents: content with mixed text and markup, where XML's tag-based structure fits better than key-value pairs
- Streaming very large datasets: a single JSON document has to load in full before most parsers can read any of it
- Binary data: images, audio, or files need base64 encoding first, which inflates the payload by roughly a third
None of this makes JSON the wrong default. It just means the format has edges, and hitting one of them is a design decision, not a flaw.
FAQ on JSON
What is JSON used for?
JSON is the data format that web APIs, configuration files, logs, message queues and document databases use to exchange structured data. It is text, so any language can read and write it, and it maps directly onto the objects, arrays, strings, numbers and booleans that every language already has. REST and GraphQL responses, package.json and tsconfig.json, PostgreSQL jsonb columns, MongoDB documents, JSON Lines log files and browser storage all use it.
Does JSON support comments?
No. RFC 8259 JSON has no comment syntax, no trailing commas, no single quotes and no unquoted keys, and a strict parser rejects all of them. Editors and tools that accept comments are reading JSONC (VS Code settings, tsconfig.json) or JSON5 (Babel and some build configs), which are supersets. If a file needs comments, use one of those formats deliberately, or a key such as _comment that the consumer ignores, or move to YAML or TOML.
What is the difference between JSON and a JavaScript object?
JSON is text; a JavaScript object is a value in memory. JSON.parse turns the text into an object and JSON.stringify turns an object back into text. JSON also allows less than JavaScript: keys must be double quoted strings, there are no comments, functions, undefined, NaN, Infinity, Dates or BigInt, numbers cannot have a leading plus or trailing dot, and the top level value can be any JSON value, not only an object. Values that JSON cannot represent are dropped or turned into null when you stringify them.
How do I validate JSON?
For syntax, parse it: JSON.parse in JavaScript, json.loads in Python, jq empty on the command line, or the validator built into this page, all of which report the position of the first error. For structure, write a JSON Schema that declares the required keys, types, formats and ranges, and validate documents against it with Ajv in JavaScript, jsonschema in Python, or a schema aware editor. Most API frameworks validate request bodies against a schema automatically.
Should I use JSON or YAML for configuration?
Use JSON when a machine writes the file, when it travels over HTTP, or when strict parsing matters: it has one grammar, every language parses it identically, and it cannot surprise you. Use YAML when humans edit the file often and need comments, multi line strings and anchors, accepting that its grammar is large and that unquoted values such as no, 1.10 or 3e2 can change type. TOML sits between them for small config files. JSON5 or JSONC give JSON comments and trailing commas when you control the parser.
How do I work with very large JSON files?
Do not load them in one piece. Stream them with an incremental parser such as ijson in Python, JSONStream or stream-json in Node, or Jackson's streaming API in Java, or convert them to JSON Lines so each record is one line that can be processed, split and grepped independently. Compress with gzip or Brotli over HTTP, since JSON compresses by five to ten times, and consider a binary encoding such as MessagePack or CBOR for internal traffic where size and parse time dominate.
Is this JSON cheat sheet up to date?
Yes. It follows RFC 8259 and ECMA-404 for the grammar, JSON Schema draft 2020-12, JSONPath as standardised in RFC 9535 and JSON5 1.0, and the library sections were checked against the current stable release of each language and library as of the byline date, September 21, 2026. Rows that show a superset or another format carry a chip that the strict JSON switch hides, and rows that depend on a recent release carry a green new chip naming the version.
Can I download this JSON cheat sheet as a PDF?
Yes. The page has a print stylesheet, so your browser's print command with Save as PDF produces a clean copy: the dark theme switches to a print palette, every expected output and explanation is expanded, and the search box, navigation and footer are left out. Collapse the sections you do not need first, or run a search, and only what is still visible is printed.