JSON formatting and validation: tools and best practices for developers
JSON looks deceptively simple. Two scalar types, two collection types, no schema. In practice, the format generates a surprising amount of friction — from trailing commas to UTF-16 surrogate pairs to the question of whether NaN is allowed. This is the working developer’s playbook.
Syntax in one paragraph
JSON has six grammar productions: object ({}), array ([]), string (double-quoted), number (no leading zeros, no NaN, no Infinity), true/false, and null. Object keys are strings. Whitespace is permitted between tokens. That is the whole spec. RFC 8259 is the canonical reference and runs to twenty pages, most of which are about edge cases in numbers and strings.
The seven most common mistakes
- Trailing commas.
[1, 2, 3,]is valid JavaScript but invalid JSON. Parsers reject it with a single-line error that often points at the wrong token. If you are hand-editing JSON, trailing commas are the most likely thing to break. - Unquoted keys.
{ foo: 1 }is JavaScript, not JSON. Keys must be double-quoted strings:{ "foo": 1 }. - Single quotes.
{ 'foo': 'bar' }— familiar from Python and YAML — is also invalid. JSON strings use double quotes only. NaNandInfinity. Both are realNumbervalues in JavaScript and Python but have no representation in JSON.JSON.stringify(NaN)returns the string"null", which is rarely what you want. Decide at serialization time how you want to encode them — usually asnullor as a sentinel string like"NaN"— and document the choice.- Numbers as strings, silently. A common API mistake: returning
{ "count": "42" }when the consumer expects{ "count": 42 }. JavaScript’s==hides the bug;===exposes it. Schema validation catches it before it ships. - Comments. JSON has no comments.
// like thisand/* like this */both break the parser. JSON5 and JSONC allow them, but plain JSON does not. - Duplicate keys.
{ "x": 1, "x": 2 }parses, but the result is implementation-defined: most parsers keep the last value, some keep the first, and some throw. Never emit duplicate keys.
JSON5, JSONC, and strict JSON
JSON5 is a JSON superset that adds comments, trailing commas, unquoted keys, single-quoted strings, hex numbers, and NaN/Infinity. It exists because JSON was never designed as a human-editable config format and the constraints chafe in that role. JSONC is JSON-with-Comments, used by VS Code (tsconfig.json, .vscode/settings.json) and a few other tools — it adds // and /* */ but nothing else.
The rule we follow: strict JSON on the wire, JSON5 or JSONC for configuration files only. If your API returns JSON5, you have forfeited interoperability with every consumer that uses a stock parser. If your config file is strict JSON, your colleagues will be annoyed every time they want to add a comment. Pick the right tool per use case.
JSON Schema for validation
A schema is the contract between a producer and a consumer. JSON Schema is the de facto standard, with implementations in every major language. A minimal schema for a user object:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["id", "email"],
"properties": {
"id": { "type": "string", "format": "uuid" },
"email": { "type": "string", "format": "email" },
"age": { "type": "integer", "minimum": 0, "maximum": 150 }
},
"additionalProperties": false
}
Three keywords pay for the cost of writing the schema:
required— the most common runtime bug is “field that I thought was always present is sometimes missing.”requiredturns that bug into a validation failure.additionalProperties: false— rejects typos in keys. Without it,{ "emial": "..." }passes validation and silently sets nothing.format— lightweight semantic check. Use it foremail,uuid,date-time,ipv4.
For TypeScript codebases, generate types from the schema (json-schema-to-typescript) rather than maintaining both by hand. For Python, jsonschema is fast enough for request-level validation. For high-throughput pipelines, fastjsonschema compiles schemas to Python and is one to two orders of magnitude faster than the reference library.
Pretty-printing rules that matter
Pretty-printing is mostly cosmetic, but a few choices have real consequences:
- Indent: two spaces is now the dominant convention —
JSON.stringify(obj, null, 2),json.dumps(obj, indent=2), and jq’s default. Four spaces is fine but produces noticeably larger diffs in code review. Tabs work but are inconsistent with how most tooling pretty-prints by default; we recommend against them for JSON. - Sorted keys: sort by default in any file that ends up in version control.
jq --sort-keysor Python’ssort_keys=True. Sorted keys produce minimal diffs when you change one field, and they make merge conflicts dramatically easier to resolve. - Trailing newline: end the file with a newline. POSIX tools assume it;
gitwill warn if it is missing. - ASCII vs. Unicode: Python’s
json.dumpsdefaults toensure_ascii=True, which escapes every non-ASCII character ("café"instead of"café"). Passensure_ascii=Falsefor human-readable output, but be aware that the byte length changes and some downstream tools assume ASCII-only.
Our JSON beautifier applies all four conventions by default and lets you toggle each one.
A debug workflow for malformed payloads
You get an error like Unexpected token y in JSON at position 12473. The position is a byte offset into the response. Here is the loop that resolves this fastest:
- Save the raw response. Do not paste into a beautifier yet — the beautifier will give up at the first error.
- Inspect the byte around the error. Open the file in a text editor that shows column numbers and jump to the position. Common causes at this stage: a literal newline inside a string, an unescaped backslash, or a smart-quote (
’) where a straight quote should be. - Check the content type. Did you actually get JSON, or HTML? A surprising number of “malformed JSON” reports turn out to be 502 error pages from a load balancer.
- Check the encoding. If the file looks fine in one editor and corrupted in another, you have a UTF-8 vs Latin-1 problem. JSON is UTF-8 by spec; if the producer is sending Latin-1, fix that at the source.
- Bisect. If you cannot find the bad byte, split the file in half and validate each half. Repeat until you have isolated the broken record. Our diff tool is useful for comparing a known-good response against the broken one.
Escape semantics in strings
Six escape sequences have specific meanings: \", \\, \/, \b, \f, \n, \r, \t, and \uXXXX. The slash escape \/ is legal but unnecessary — it exists so that JSON can be safely embedded inside </script> tags without an HTML parser closing the script element early.
The interesting case is \uXXXX. It encodes a single UTF-16 code unit, not a Unicode code point. Code points above U+FFFF require a surrogate pair: 😀 for the grinning-face emoji U+1F600. If you write \uD83D alone, you have an unpaired surrogate — most JSON parsers accept it, but the resulting string is not valid Unicode and will break downstream consumers. Always validate that surrogates are paired.
For escaping arbitrary strings into and out of JSON-safe form, our escape/unescape tool handles both JSON and JavaScript escape conventions side by side.
Performance with large payloads
JSON parsing is fast in absolute terms but slow relative to binary formats. Three rules of thumb at scale:
- Stream, do not buffer. For payloads larger than ~50 MB, do not
JSON.parsethe whole document. Use a streaming parser (oboe.js,ijsonin Python,jq --stream) that emits events per token. Memory drops from O(document size) to O(depth). - Skip fields you do not need. If the payload has a 100 MB
raw_htmlfield and you only wantid, a streaming parser can ignore the large field entirely and finish in a fraction of the time. - Reach for binary. If you control both ends and JSON is the bottleneck, consider MessagePack, CBOR, or Protobuf. Typical savings: 30–50% on size, 2–5x on parse time.
One trap: JSON.parse in V8 is highly optimized, often beating naive streaming approaches for small documents. Always benchmark with realistic data before switching parsers.
For tabular transforms, our JSON-to-CSV converter handles the common case of flattening an array of objects into a spreadsheet-friendly format.
When to use jq vs. an in-browser tool
jq is the command-line workhorse. Use it when:
- The file is larger than a few megabytes — browser tools start to lag.
- You need to script the transformation.
jqprograms are composable and easy to put under version control. - You want streaming behaviour:
jq --stream, orjq --argfor parameterized queries from a shell loop.
Use an in-browser tool when:
- You are exploring an unfamiliar payload. Visual indentation, fold/unfold, and search beat
jqfor orientation. - You need to validate against a schema interactively.
- You want to share the result with someone who does not have
jqinstalled.
Both are legitimate. The jq command for “pretty-print and sort keys” is jq --sort-keys . file.json; the equivalent in our beautifier is one click.
Closing
JSON is the lingua franca of web APIs because it is good enough for most jobs and supported everywhere. The cost of that ubiquity is that every team eventually trips on the same handful of edge cases: trailing commas, unquoted keys, NaN, UTF-16 surrogates, duplicate keys. The shortcut is to enforce strict JSON with a schema at the boundary, pretty-print with sorted keys in source control, and reach for the right tool — jq or a browser — when something breaks. None of these are hard; they just compound when teams skip them.