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.

26sections
212snippets
6value types
0signup needed

Updated September 21, 2026, checked against RFC 8259, ECMA-404, JSON Schema 2020-12 and the current stable releases of every library on the page. Print it for a PDF copy.

/
level

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.

01

Syntax & Data Types

the whole grammar, six value types, a playground

Playground: Format, Minify, Validate

core
Ctrl+Enter formats; an error selects the offending character
output

          
Paste JSON above, then pick an action.

The Whole Grammar

RFC 8259core

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

core
TypeExampleJavaScriptPythonNotes
object{"a": 1}Objectdictunordered by spec, ordered in practice
array[1, "a", null]Arraylistordered, heterogeneous
string"caf\u00e9"stringstrUnicode, double quotes, escapes
number-12.5e3number (IEEE 754 double)int or floatno int/float distinction in the grammar
booleantruebooleanTrue / Falselowercase in JSON
nullnullnullNonea 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

everyday

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
02

Strings & Escapes

quotes, the nine escapes, Unicode, dates, binary

Escapes

core

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

everyday

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
03

Numbers, Booleans & null

precision limits, money, big ids, null versus absent

Numbers

core

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

core

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)
04

Objects & Arrays

nesting, ordering, shapes that work and shapes that hurt

Rules

core

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

everyday

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

everyday

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}
05

Valid vs Invalid JSON

every classic mistake next to its fix

Side by Side

core
InvalidWhyValid
{'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} // notecomment{"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
break"}
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
"unterminatedmissing closing quote"terminated"
{"a": [1, 2}bracket mismatch{"a": [1, 2]}
(empty file)no value at allnull, {} or []
{"a": 1}byte order marksave as UTF-8 without BOM

Find the Error Fast

everyday

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

everyday

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
06

JavaScript

parse, stringify, reviver, replacer, fetch, files

JSON.parse and JSON.stringify

core

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

everyday

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

advanced

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

everyday

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))
07

TypeScript

typing parsed JSON, runtime validation, JSON modules

Types for JSON

everyday

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

everyday

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
08

Python

the json module, dataclasses, pydantic, orjson

The json Module

core

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

everyday

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"]
09

PHP

json_encode, json_decode, flags, JsonSerializable

Encode and Decode

core

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

everyday

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
10

Java & Kotlin

Jackson, Gson, kotlinx.serialization, Spring

Jackson

everyday

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

everyday

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();
11

C# & .NET

System.Text.Json, Json.NET, ASP.NET Core

System.Text.Json

everyday

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

everyday

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
12

Go, Rust & Ruby

encoding/json, serde, the JSON gem, and a few more

Go: encoding/json

everyday

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

everyday

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

everyday

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
13

jq

the command line JSON processor, from dot to reduce

Basics

jqcore

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

jqeveryday

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")'
14

Command Line & Editors

curl, json.tool, gron, fx, VS Code, diffs

Shell Tools

core

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

everyday

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 ."
15

JSON Schema

describe, validate and document a document

A Complete Schema

2020-12core

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

everyday
Applies toKeywords
anytype (or an array of types), enum, const, default, examples, title, description, deprecated, readOnly, writeOnly
stringminLength, maxLength, pattern (ECMA regex, unanchored), format, contentEncoding, contentMediaType
number, integerminimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf
objectproperties, required, additionalProperties, patternProperties, propertyNames, minProperties, maxProperties, dependentRequired, dependentSchemas, unevaluatedProperties
arrayitems, prefixItems (tuples), contains, minContains, minItems, maxItems, uniqueItems, unevaluatedItems
combiningallOf, 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

everyday

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" }
16

Pointer, Patch & JSONPath

address a value, describe a change, query a document

JSON Pointer

RFC 6901everyday

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

RFC 6902 / 7386everyday

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

RFC 9535everyday

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
17

Spreadsheets, Postman & Automation

JSON without writing a program

Excel and Google Sheets

everyday

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

everyday

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

everyday

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
18

APIs & HTTP

headers, error shapes, conventions that survive

Headers and Status

core

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

everyday

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

everyday

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
19

JSON Lines, JSON5 & JSONC

the supersets and siblings you will meet

JSON Lines / NDJSON

.jsonleveryday

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

everyday

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}
20

Databases

PostgreSQL jsonb, MySQL, SQLite, SQL Server, MongoDB

PostgreSQL jsonb

everyday

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

everyday

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

everyday

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

everyday

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
21

Config Files & Conventions

the JSON files every project has, and a style guide

Files You Will Edit

everyday

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

everyday

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

everyday

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
22

Structured Output & AI

getting valid JSON out of a language model, and checking it

Ask for a Schema, Not for JSON

everyday

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

everyday

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
23

Security

what can go wrong with untrusted JSON, and the fix for each

Parsing Untrusted Input

everyday

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

everyday

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

everyday

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
24

Performance & Big Files

streaming, compression, faster parsers, binary cousins

Streaming Instead of Loading

everyday

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

everyday

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)
25

Errors & Debugging

the messages, what they mean, and where to look

Error Messages Decoded

core
MessageWhereUsual causeFix
Unexpected token < in JSON at position 0JavaScriptthe server sent HTML: a 404, a login page, an error pagecheck res.ok and res.headers.get("content-type") before res.json()
Unexpected token u in JSON at position 0JavaScriptyou parsed the string "undefined": the variable was never set, or localStorage key is missingguard with ?? "null" or check for null first
Unexpected end of JSON inputJavaScriptempty body, truncated response, unbalanced bracketsread res.text() and look; check Content-Length and timeouts
Unexpected token ' in JSONJavaScriptsingle quoted strings or keysdouble quotes
Unexpected token } / ] in JSONJavaScripttrailing comma before the closing bracketremove the comma
Bad control character in string literalJavaScripta raw newline or tab inside a stringescape as \n / \t
Converting circular structure to JSONJavaScriptan object references itself (DOM nodes, ORM models)a replacer, toJSON, or serialise a plain projection
Do not know how to serialize a BigIntJavaScripta BigInt in the datareplacer to string, or JSON.rawJSON
Expecting property name enclosed in double quotesPythonsingle quotes, unquoted keys, or a trailing comma in an objectfix the source; if it is Python repr, use ast.literal_eval not json
Expecting value: line 1 column 1 (char 0)Pythonempty string, HTML, or a BOM at the startprint repr(text[:80]); open with encoding="utf-8-sig" for BOM files
Object of type datetime is not JSON serializablePythona date, Decimal, UUID, set or dataclass in the datadefault=str or a proper default function
Extra data: line 2 column 1Pythontwo documents in one string, usually JSON Linesparse per line, or wrap in an array
Syntax error (4) / json_decode returns nullPHPinvalid JSON, or valid JSON nullJSON_THROW_ON_ERROR; json_last_error_msg()
Malformed UTF-8 characters (5)PHPLatin-1 bytes from a database or filemb_convert_encoding($s, 'UTF-8', 'ISO-8859-1') or JSON_INVALID_UTF8_SUBSTITUTE
Unrecognized field "x" not marked as ignorableJacksonthe API added a fieldFAIL_ON_UNKNOWN_PROPERTIES = false or @JsonIgnoreProperties(ignoreUnknown = true)
Cannot deserialize value of type X from StringJacksona number sent as a string, or a date in an unexpected formatcoercion config, @JsonFormat, or fix the producer
The JSON value could not be converted to X.NETtype mismatch, or camelCase vs PascalCase with case sensitive matchingJsonSerializerDefaults.Web, NumberHandling.AllowReadingFromString
json: cannot unmarshal string into Go value of type intGotype mismatchmatch the struct type, or json.Number / a custom UnmarshalJSON
invalid character 'x' looking for beginning of valueGonot JSON at all, often HTML or a BOMlog the first bytes of the body
parse error: Invalid numeric literal at line 1, column 8jqunquoted string or key, single quotes, or the input is not JSON (YAML, text)jq reads plain JSON only; -R for raw lines

Debugging Checklist

everyday

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

everyday

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]
26

JSON vs YAML, XML & More

when each format wins, the spec history, a glossary

Format Comparison

core
FormatCommentsSchemaTypesHuman editingSizeBest for
JSONnoJSON Schema6, no datesfairmediumAPIs, interchange, storage, anything a machine writes
JSON5 / JSONCyesJSON SchemaJSON + Infinity, NaNgoodmediumconfig files you control the parser for
JSON Linesnoper lineJSONfairmediumlogs, exports, streams, datasets
YAMLyesJSON Schema (via tools)many, guessedgood until it is notsmallCI pipelines, Kubernetes, Docker Compose, human edited config
TOMLyesnone standardexplicit, datesgood for flat configmediumCargo, pyproject, app settings
XMLyesXSD, RelaxNGtext + schemaverboselargedocuments, SOAP, legacy enterprise, mixed content
CSVnononetext onlyin a spreadsheetsmallflat tables, Excel, imports
Protobufin .protorequiredrich, typedno (binary)tinygRPC, internal services, mobile
MessagePack / CBORnononeJSON + binary, datesno (binary)smalldrop in binary JSON
Parquetnoin the filecolumnar, typednotinyanalytics, 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

everyday

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

everyday
YearMilestoneWhat changed
2001Douglas Crockford names JSONa subset of JavaScript object literals for State Software's data exchange
2002json.orgthe one page grammar and the railroad diagrams
2005AjaxJSON replaces XML in browser requests; the term Ajax is coined
2006RFC 4627first informational RFC; application/json; top level must be object or array
2009ES5JSON.parse and JSON.stringify become part of JavaScript; MongoDB and CouchDB store JSON
2013ECMA-404Ecma standardises the syntax alone
2014RFC 7159 / RFC 7493any value at the top level; I-JSON profile for interoperability (UTF-8, doubles, unique keys)
2017RFC 8259current standard; UTF-8 required for open exchange; ISO/IEC 21778:2017 published the same grammar
2018JSON5 1.0the human friendly superset gets a spec
2020JSON Schema 2020-12current schema draft; adopted by OpenAPI 3.1 in 2021
2024RFC 9535JSONPath standardised after 17 years of dialects
2024JSON.rawJSON, JSON.parse source accessES2024 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.

TypeExampleNotes
String"city": "London"Always double-quoted
Number"age": 45No leading zeros, no hex
Boolean"active": trueOnly true or false
Null"middleName": nullRepresents 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.

ToolChecks SyntaxChecks SchemaBest For
JSONLintYesNoQuick syntax checks
JSON SchemaNoYesStructural and type validation
Visual Studio CodeYesPartialInline 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.

FormatVerbosityNative Data TypesCommon Use Case
JSONLowString, number, boolean, null, object, arrayREST API payloads
XMLHighText only, types inferred by schemaSOAP APIs, legacy enterprise systems
YAMLLowSame core types as JSON, plus datesConfiguration 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.