JSON Formatting Best Practices: How to Write Clean, Readable JSON
The best practices for formatting JSON are: follow the strict syntax (double quotes, no trailing commas, no comments), save files as UTF-8 without a byte order mark, indent files that people read with two spaces, minify what you send over a network, use one key naming style everywhere, and validate before you commit. Breaking any of the syntax rules makes the whole document invalid, so most "JSON bugs" come down to a few small slips.
This guide shows each rule with the real error your parser prints when you break it. The examples were run on Node.js 22, Python 3.11, and jq 1.8, and the size and speed figures come from our own test on 1,000 generated records. To check a document right now, paste it into our JSON Formatter, which validates, formats with two-space indentation, and minifies.
JSON Syntax Rules (and the Mistakes That Break Parsers)
JSON's grammar is small: objects, arrays, strings, numbers, true, false, and null, as defined in RFC 8259. The strictness is where people stumble. These are the rules you must follow.
1. No Trailing Commas
This is the most common JSON error. JavaScript allows trailing commas in object literals and arrays, so developers assume JSON does too. It does not: the RFC 8259 grammar allows a comma only between two values.
// INVALID: trailing comma after "admin"
{
"name": "Alice",
"role": "admin",
}
// VALID
{
"name": "Alice",
"role": "admin"
}
Node.js 22 reports the object case as Expected double-quoted property name in JSON at position 40 (line 4 column 1) and the array case ([1,2,3,]) as Unexpected token ']'. Older engines say things like "Unexpected token }", which is why a trailing comma is easy to miss.
2. Strings Must Use Double Quotes
JSON requires double quotes around every key and every string value. Single quotes and unquoted keys are invalid.
// INVALID: single quotes
{'name': 'Alice'}
// INVALID: unquoted key
{name: "Alice"}
// VALID
{"name": "Alice"}
This trips up Python developers in particular. json.dumps() writes valid JSON (true, null, double quotes), but printing a Python dict gives {'ok': True, 'v': None}, which is not JSON. Always use your language's JSON serializer instead of string concatenation or print().
3. No Comments
JSON has no comment syntax: no //, no /* */, no #. Douglas Crockford, who popularized the format, has said he removed comments because people were using them to hold parsing directives, which would have destroyed interoperability (quoted from his 2012 post).
// INVALID: comments are not allowed
{
// user's display name
"name": "Alice"
}
// Workaround: a descriptive key or a "_comment" field
{
"_comment": "Configuration for the auth service",
"tokenExpiry": 3600
}
The _comment key is real data, so it is shipped and parsed like any other field. If you need comments in a config file, use a format that supports them: JSONC (JSON with Comments), which VS Code uses for settings.json, tasks.json, and launch.json, or JSON5, YAML, or TOML. Note that package.json is plain JSON and does not allow comments.
4. Numbers Follow a Strict Format
A JSON number has no leading zeros, no leading plus sign, no hex, no NaN or Infinity, and needs digits on both sides of a decimal point (so .5 and 5. are invalid).
// INVALID
{"port": 08080} // leading zero
{"value": NaN} // NaN not allowed
{"value": Infinity} // Infinity not allowed
{"value": +5} // leading plus sign
{"value": .5} // no digit before the point
// VALID
{"port": 8080}
{"value": null} // use null instead of NaN
{"value": 5}
{"value": 0.5}
Watch out for big integers too. JavaScript reads every JSON number as a double-precision float, so 12345678901234567890 becomes 12345678901234567000. RFC 8259 notes that integers are only guaranteed to match across software between plus and minus 9,007,199,254,740,991 (253 minus 1). Send larger IDs as strings.
5. Escape Special Characters in Strings
A string cannot contain a raw newline or tab, and a backslash must be escaped. Use \n for a newline and \\ for a backslash.
// INVALID: raw line break inside a string
{"message": "Hello
World"}
// INVALID: unescaped backslashes in a Windows path
{"path": "C:\Users\tom"}
// VALID
{"message": "Hello\nWorld"}
{"path": "C:\\Users\\tom"}
Node.js 22 reports these as Bad control character in string literal in JSON at position 18 and Bad escaped character in JSON at position 13. Generating JSON with a serializer instead of building strings by hand avoids both.
6. Use UTF-8 Without a Byte Order Mark
RFC 8259 says JSON exchanged between systems must be encoded as UTF-8 and that implementations must not add a byte order mark (BOM) to the start of a networked JSON text. Parsers may ignore a BOM, but many do not: JSON.parse rejects it, and Python's json.loads raises Unexpected UTF-8 BOM (decode using utf-8-sig) when it is given a string that starts with one, for example a file opened with encoding='utf-8'.
If an editor or export tool added a BOM to a file, strip it before parsing. In Node.js, text.replace(/^\uFEFF/, '') does it, and in Python you can open the file with encoding='utf-8-sig'.
7. Keep Keys Unique
RFC 8259 says the names inside an object should be unique and that software receiving duplicates behaves unpredictably. JavaScript and Python both keep the last value ({"a":1,"a":2} parses as a = 2), but other parsers may keep the first, reject the document, or do something else. Because JSON.parse silently keeps the last value, quick checks (including our JSON Formatter) will not flag duplicates and will drop the earlier value when they reformat, so look for them in code review or with a linter that reports them.
Pretty-Print vs. Minified: When to Use Each
JSON can be written human-readable (pretty-printed) or compact (minified). The data is identical; only whitespace differs.
// Pretty-printed (readable)
{
"user": {
"id": 1042,
"name": "Alice Chen",
"roles": ["admin", "editor"]
}
}
// Minified (compact)
{"user":{"id":1042,"name":"Alice Chen","roles":["admin","editor"]}}
How much does the whitespace cost? We generated 1,000 user records (id, names, email, roles, address, timestamps) and measured the same data in three layouts, with and without gzip compression at its default level.
| Layout | Raw size | Gzipped size |
|---|---|---|
| Minified | 224.7 KB | 30.9 KB |
| Pretty, 2 spaces | 321.2 KB (43% larger) | 32.7 KB (6% larger) |
| Pretty, 4 spaces | 390.2 KB (74% larger) | 33.2 KB (7% larger) |
Minifying removed 30% of the bytes from the 2-space file, but once the response is compressed the gap shrinks to about 6%, because repeated whitespace compresses very well. Your data will give different numbers, but the pattern is worth knowing: minify when you pay for raw bytes or cannot compress, and do not give up readability for a few percent when you can.
| Use Case | Format | Why |
|---|---|---|
| API responses | Minified, compressed by the server | Machines read it, and every byte is sent on every request |
Config files you edit (package.json) |
Pretty, 2 spaces | People read and edit these |
| Git-tracked JSON | Pretty, with keys in a stable order | Produces clean, reviewable diffs (try our Diff Checker) |
| Log files | One minified object per line | Line-based tools such as grep and jq read logs one line at a time; pretty-print a single entry when you need to read it |
| Examples in docs and tutorials | Pretty | Readers need to see the structure |
In most languages, switching between the two is a single argument:
// JavaScript
JSON.stringify(data, null, 2) // pretty (2-space indent)
JSON.stringify(data) // minified
# Python
json.dumps(data, indent=2, sort_keys=True) # pretty, keys sorted for stable diffs
json.dumps(data, separators=(',', ':')) # minified
Nested Object Best Practices
Deep nesting makes JSON hard to read, hard to query, and hard to maintain. A common guideline is to reconsider the design once you are more than three or four levels deep. RFC 8259 also lets parsers cap the nesting depth they accept, so very deep documents can fail in some tools.
// Too deep: five objects down to reach one value
{
"company": {
"departments": {
"engineering": {
"teams": {
"frontend": {
"lead": "Alice"
}
}
}
}
}
}
// Flattened with clear key names
{
"companyName": "Acme Corp",
"engineeringFrontendLead": "Alice",
"engineeringFrontendSize": 8
}
// Moderate nesting with an array of objects
{
"company": "Acme Corp",
"teams": [
{
"department": "engineering",
"name": "frontend",
"lead": "Alice",
"size": 8
}
]
}
The array version is easy to loop over, filter, and extend with new teams without inventing new keys. When you design a JSON API, think about how the consumer will read the data: an array of objects is almost always easier to work with than a deep tree of named keys. If you work in TypeScript, our JSON to TypeScript converter turns a sample payload into interfaces so you can see how awkward a shape is to type.
Naming Conventions: camelCase vs. snake_case
RFC 8259 says nothing about key naming, so consistency within a project is what matters. Here is what well-known ecosystems do:
| Convention | Example | Where you see it |
|---|---|---|
| camelCase | firstName |
JavaScript and TypeScript code, the Azure REST API guidelines, and the web defaults of .NET's System.Text.Json |
| snake_case | first_name |
Python and Ruby code, plus the Stripe and GitHub REST APIs |
| kebab-case | first-name |
Rare in JSON, because JavaScript needs bracket notation to read it |
| PascalCase | FirstName |
What System.Text.Json writes by default for C# properties, because it leaves names unchanged unless you set a naming policy |
Pick one and enforce it. If your backend is Python and your frontend is JavaScript, decide at the API boundary. Many teams send snake_case from the API and convert to camelCase in the client, with a library such as camelcase-keys on the JavaScript side or an alias generator in Pydantic on the Python side. Converting keys by hand at every call site is where mixed styles sneak in.
Conventions for Values
- Dates: JSON has no date type, so use ISO 8601 strings in UTC, such as
2026-09-30T14:30:00Z. JavaScript'sJSON.stringifywritesDateobjects in this format, with milliseconds (2026-09-30T14:30:00.000Z). - Money: floating-point math is inexact (
0.1 + 0.2is0.30000000000000004), so send amounts as integer cents or as decimal strings. - Booleans and missing values: use real
trueandfalse, not the strings "true" and "false". Usenullfor "no value".JSON.stringifyturnsNaNandInfinityintonulland silently drops keys whose value isundefined. - Large IDs: send integers above 9,007,199,254,740,991 as strings, as noted in the number rules above.
Common Validation Errors and How to Fix Them
When your JSON does not parse, the error message is often cryptic. The table shows the messages Node.js 22 prints for the most frequent mistakes, each tested on the input in the first column (it also adds the line and column after the position, for example "(line 1 column 8)"). Other engines word them differently, but they all point to a position in the text, and the number counts characters from zero.
| Input | Node.js 22 error | Fix |
|---|---|---|
{"a":1,} |
Expected double-quoted property name in JSON at position 7 | Remove the comma before the closing brace |
{'a':1} or {a:1} |
Expected property name or '}' in JSON at position 1 | Put double quotes around every key and string |
{"a":1 "b":2} |
Expected ',' or '}' after property value in JSON at position 7 | Add the missing comma between members |
{"a":1 |
Expected ',' or '}' after property value in JSON at position 6 | Close every open brace and bracket; the two must match |
{"a":1}} |
Unexpected non-whitespace character after JSON at position 7 | Delete the extra closing brace or text after the document |
{"a": undefined} |
Unexpected token 'u' | Use null; JSON has no undefined |
A server reply that starts with <!DOCTYPE html> |
Unexpected token '<', "<!DOCTYPE "... is not valid JSON | The server sent an HTML error page; check the URL, the status code, and the Content-Type header |
| An empty string (for example a 204 response) | Unexpected end of JSON input | Check that the body is not empty before you parse it |
JSON.parse(undefined) |
"undefined" is not valid JSON | A variable was never set; find where the text should have come from |
The fastest way to find a problem in a long document is to paste it into the JSON Formatter, which shows the parser's error with an approximate line and column. For formal validation of structure (required fields, types, ranges), use JSON Schema, which checks the shape of the data as well as the syntax.
JSON vs. YAML vs. TOML: Which Format Should You Use?
JSON is not the only structured text format. Here is how it compares with the two most common alternatives for configuration:
| Feature | JSON | YAML | TOML |
|---|---|---|---|
| Comments | No | Yes (#) | Yes (#) |
| Multi-line strings | No (use \n) | Yes (| and >) | Yes (triple quotes) |
| Trailing commas | No | Not needed in block style | In arrays; in inline tables only since TOML 1.1 |
| Date and time type | No (use strings) | Depends on the parser | Yes |
| Whitespace-sensitive | No | Yes (indentation is syntax) | No |
| Parser availability | In the standard library of JavaScript, Python, Go, PHP, and Ruby | A library in most languages | A library in most languages (Python 3.11 and later reads it with tomllib) |
| Typical use | APIs and data exchange | Config and CI files (Docker Compose, GitHub Actions, Kubernetes) | App settings (Cargo.toml, pyproject.toml) |
Rule of thumb: use JSON for APIs and data interchange. Use YAML for files that humans edit often and that need comments. Use TOML for application settings where you want something cleaner than JSON and less error-prone than YAML. To move between formats, try the YAML to JSON converter, or the CSV to JSON converter for tabular data.
Speed is another difference. In a quick test in Python 3.11 on the same 1,000 records, json.loads took about 1 ms, the standard library's XML parser took 3.4 ms, PyYAML's safe_load took 381 ms, and PyYAML with the libyaml C loader took 55 ms. Results vary by language and library, but YAML's much larger grammar makes it the slowest of the three to parse in this test.
Working With Large JSON Files
Once a JSON file grows past a few megabytes, parsing it in one call starts to hurt. JSON.parse needs the whole text as one string, and in Node.js 22 the longest string allowed (buffer.constants.MAX_STRING_LENGTH) is 536,870,888 characters, roughly half a billion. A larger text cannot be parsed in a single call. Here are practical strategies.
Use Streaming Parsers
A streaming parser hands you one record at a time, so memory use stays flat. This example uses stream-json 3.x (npm install stream-json stream-chain) on a file that holds one big array:
// Node.js (ES modules): stream-json processes items one at a time
import fs from 'node:fs';
import chain from 'stream-chain';
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/stream-array.js';
const pipeline = chain([
fs.createReadStream('huge-data.json'),
parser(),
streamArray()
]);
pipeline.on('data', ({ value }) => processItem(value));
In Python, ijson does the same job. Open the file in binary mode and iterate over the items of the top-level array:
# Python: pip install ijson
import ijson
with open('huge-data.json', 'rb') as f:
for user in ijson.items(f, 'item'):
process(user)
Use jq for Command-Line Processing
jq filters, transforms, and queries JSON from the command line. By default it reads a whole document into memory (its --stream option parses incrementally), but it processes a JSON Lines file one record at a time.
# Pretty-print a file
jq '.' data.json
# Extract all user names
jq '.[].name' users.json
# Keep only users with the role "admin"
jq '[.[] | select(.role == "admin")]' users.json
# Count items in an array
jq length data.json
# Convert an array to JSON Lines, one object per line
jq -c '.[]' users.json > users.jsonl
Consider JSON Lines (JSONL) for Large Datasets
Instead of wrapping everything in one giant array, put one complete JSON value on each line. The JSON Lines format requires UTF-8 with no BOM, one valid JSON value per line, and a newline between lines, and files usually end in .jsonl. It is much easier to stream, split, append to, and process in parallel.
// Traditional JSON array: must be parsed as one document
[
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
{"id": 3, "name": "Charlie"}
]
// JSON Lines (.jsonl): parse line by line
{"id": 1, "name": "Alice"}
{"id": 2, "name": "Bob"}
{"id": 3, "name": "Charlie"}
Reading it in Node.js takes a few lines with the built-in readline module:
import fs from 'node:fs';
import readline from 'node:readline';
const lines = readline.createInterface({
input: fs.createReadStream('users.jsonl'),
crlfDelay: Infinity
});
for await (const line of lines) {
if (line.trim()) processItem(JSON.parse(line));
}
Data tools expect this layout too. BigQuery's documentation says JSON input must be newline-delimited (the same format as JSON Lines), and Amazon Athena's Hive and OpenX JSON SerDe libraries expect each record on a single line, separated by a newline.
JSON Formatting Checklist
Before you commit or send JSON, run through this list:
- Every key and string uses double quotes, and backslashes and line breaks are escaped
- No trailing commas and no comments (use JSONC, YAML, or TOML if you need comments)
- Saved as UTF-8 without a byte order mark
- One naming convention (camelCase or snake_case), never a mix
- Dates as ISO 8601 strings, money as integer cents or strings, large IDs as strings
- No duplicate keys
- Nesting no deeper than three or four levels where you can avoid it
- Pretty-printed with a fixed indent for files people read, minified for responses, one object per line for logs and big datasets
- Validated with a parser, the JSON Formatter, or a JSON Schema
The same habit of formatting before you commit helps with other languages, too: our SQL formatting guide covers it for queries, and the developer tools collection has more formatters and converters.
Frequently Asked Questions
Can JSON contain comments?
No. The JSON specification (RFC 8259) has no comment syntax, so a parser will reject // and /* */. If you need comments in a JSON-like file, use JSONC (VS Code reads it for its settings files), JSON5, YAML, or TOML. A "_comment" key works as a workaround, but it becomes part of your data.
Are trailing commas allowed in JSON?
No. RFC 8259 allows a comma only between two values, so a comma after the last item makes the document invalid and strict parsers fail with an error such as "Expected double-quoted property name". Some supersets and neighbors allow them: JSONC and JSON5 do, and TOML allows them in arrays. Remove the comma, or use a formatter that does it for you.
Should I use camelCase or snake_case for JSON keys?
Either is fine, and the standard does not pick one. Follow the convention of your API's ecosystem: camelCase is common with JavaScript and the Azure REST API guidelines, and snake_case is used by the Stripe and GitHub REST APIs. What matters is using one style everywhere in the same API.
Should I use 2-space or 4-space indentation for JSON?
Both are valid, so consistency matters more than the number. Two spaces is what npm init writes into package.json and what JavaScript's JSON.stringify(data, null, 2) examples usually show, and Python's json.tool defaults to four. In our test, 2 spaces made the file 43% larger than minified and 4 spaces made it 74% larger, so two spaces also keep deeply nested data narrower. Set the same indent in your formatter and editor.
What is the maximum size of a JSON file?
The JSON specification sets no limit; RFC 8259 lets each implementation choose its own. The practical limit is your parser and memory. In Node.js 22, a string can hold at most 536,870,888 characters (roughly half a billion), so JSON.parse cannot handle a larger text in one call. For big datasets, use a streaming parser such as stream-json (Node.js) or ijson (Python), or switch to JSON Lines.
Sources
- IETF: RFC 8259, The JavaScript Object Notation (JSON) Data Interchange Format
- JSON Lines format specification
- Douglas Crockford, "Why I removed comments from JSON" (2012, as quoted on Hacker News)
- Visual Studio Code: Editing JSON, JSON with Comments
- TOML v1.0.0 specification
- TOML 1.1.0 release notes
- Microsoft Azure REST API guidelines
- Microsoft Learn: Customize property names and values with System.Text.Json
- Stripe API reference: The Customer object
- GitHub REST API: Get a repository
- Google Cloud: Loading JSON data from Cloud Storage into BigQuery
- Amazon Athena: JSON SerDe libraries
- Node.js documentation: Buffer, buffer.constants.MAX_STRING_LENGTH
- stream-json and ijson (streaming JSON parsers)
- jq manual
- JSON Schema
- Pydantic: Alias generators and camelcase-keys