Skip to main content
Tools Blog

JSON Formatting: What Separates Production Code from Spaghetti

Published January 20, 2026 · 11 min read

Last month I reviewed a PR that took six hours to merge because the author had copy-pasted JSON from three different sources into a config file. Each source had different indentation. One had trailing commas. The linter was disabled in that directory. It looked fine in the editor and was completely broken when shipped.

JSON is supposed to be simple. It's just keys, values, and a few rules. And yet I've seen JSON files cause more production incidents than almost any other format — not because JSON is hard, but because the conventions are so loose that they drift without anyone noticing until something breaks.

Here's what I look for when I review JSON files, and the rules I'd actually enforce on a real project.

Indentation: Just Use Two Spaces

The JSON spec (RFC 8259) is deliberately silent on whitespace. Any whitespace is valid as long as it's consistent within a single token. This is the source of countless hours of bikeshedding. I've seen teams argue for hours about tabs vs spaces, 2 vs 4, alignment vs simple nesting. Stop. Use two spaces. Move on.

Two spaces is the default in JSON.stringify, Python's json.dumps, Ruby's to_json, and most editors' built-in formatters. It fits more nesting on screen than four spaces and is more consistent across editors than tabs. If you need to defend this in a code review, the argument is "this matches every other tool" — that's a strong argument and it's true.

Use an EditorConfig file to enforce this across your team:

[*.json]
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

This file travels with your repo, so every editor that supports EditorConfig (which is most of them) will pick up these rules automatically. No more "works on my machine" formatting issues.

Minify for Production, Pretty-Print for Development

Pretty-printed JSON is two to three times larger than minified. For a 100KB response, that's a difference of 200KB on the wire per request. At a million requests per day, you've added 200GB of bandwidth. That's not a trivial cost.

The pattern I follow: write JSON pretty-printed in your source, minify it at build time or at the API edge. Most build tools handle this. In Node.js, the JSON.stringify() function with no arguments produces minified output. In Python, json.dumps(obj, separators=(',', ':')) does the same. In Nginx, the gzip module compresses both minified and pretty-printed JSON effectively, so if you can serve gzipped, the bandwidth difference shrinks to about 5%.

For config files that humans edit, always pretty-print. For data that flows through APIs, always minify. For data stored on disk in version control (test fixtures, mock data), pretty-print — future you will need to read it.

Use Meaningful Key Names

Bad keys: u_id, uid, userId, user_id. I've seen all four in the same project. Pick a convention and enforce it. My recommendation: snake_case for JSON, regardless of what your server-side language uses. JSON conventions are set by JavaScript (camelCase) and Python/Ruby (snake_case), and there's no objective winner. But JSON itself is language-agnostic, and snake_case reads more clearly in multi-language environments.

Avoid abbreviations. Modern IDEs auto-complete. customer_id is clearer than cust_id and the time you save typing is negative once you account for lookup time. Don't optimize for line length; optimize for scan-ability.

Use plural for arrays (orders, not order). Use singular for objects. The convention matters less than the consistency, but a 30-second conversation with your team saves hundreds of "wait, is this an array or an object?" moments.

Dates: ISO 8601 or Bust

JSON has no date type. You have three options: ISO 8601 strings, Unix timestamps (in seconds or milliseconds), or your own custom format. Pick ISO 8601 unless you have a specific reason not to. "2026-01-20T14:30:00Z" parses in every language, sorts correctly as a string, and is human-readable. The Z at the end (or +00:00) makes the timezone explicit.

Unix timestamps are smaller (10 bytes vs 24) and sort numerically, but they require conversion at every boundary. If you're storing them, you need to know whether they're seconds or milliseconds — the 500x difference means a January 1970 bug if you get it wrong. I've debugged that one. It's not fun.

Custom date formats (like "01/20/2026" or "Jan 20, 2026") are an antipattern. They require a parser, they're locale-dependent, and they break in edge cases (what's "02/03/2026" — February 3 or March 2?). Don't do it.

Null vs Missing: Pick One and Document It

This is the most under-discussed JSON design decision. {"name": null} and {} (the name key is missing) are technically different, and clients have to handle both. Some APIs use null to mean "explicitly set to no value," others use it to mean "not set," and others omit the key entirely to mean "not applicable."

My recommendation: use null for "we have a value but it's empty" (e.g., a user with no middle name), and omit the key for "we don't know" (e.g., a user whose middle name hasn't been collected). That's a semantic distinction that clients can rely on. Document it in your API spec, and stick to it.

Whatever you decide, be consistent. I've seen the same API return {"deleted_at": null} and {} for "not deleted" depending on which microservice generated the response. That kind of inconsistency is what makes client code defensive and bloated.

The Trailing Comma Trap

JSON does not allow trailing commas. {"a": 1, "b": 2,} is invalid. Most JSON parsers will reject it with a helpful error pointing to the trailing comma. JavaScript allows trailing commas, so if you're generating JSON from JS code, double-check your serializer strips them.

Python's json.dumps() does it right. Go's encoding/json does it right. Most languages do it right. The exceptions are usually hand-written serializers or templates. If you're building JSON by string concatenation (don't), use a real library.

JSON5 allows trailing commas. JSON5 is a superset of JSON designed for human-edited config files. It's great for tsconfig.json, .eslintrc, and similar files. It's terrible for network payloads because most parsers don't handle it. Use JSON5 for config, strict JSON for everything else.

Validate with a Schema

If your JSON has a stable structure, define a schema. JSON Schema is a standard for describing what valid JSON looks like — required keys, type constraints, regex patterns for strings, value ranges for numbers, even conditional requirements. It's verbose to write but invaluable for maintaining API contracts.

For a public API, schemas are non-negotiable. They catch breaking changes before they ship, they generate documentation automatically, and they let clients generate type-safe bindings. The upfront cost is paid back the first time you accidentally change a field type and your schema validator catches it in CI.

For internal data files, schemas are still valuable. A schema for a config file means a typo in a key fails fast instead of silently being ignored. A schema for a test fixture means you can't accidentally save invalid data that passes tests but breaks production.

Popular libraries: Ajv (JavaScript, fast), jsonschema (Python, simple), JSON.NET Schema (C#), and many more. The syntax is the same across languages; only the API differs.

Watch Out for These Five Pitfalls

These are the ones I've seen cause real outages, in order of frequency:

1. Mixed array types. [{"id": 1}, "string", null] is technically valid but almost always a bug. Arrays should be homogeneous. If you need different types, use a discriminator key: [{"type": "user", "id": 1}, {"type": "guest", "id": "abc"}]. This makes client code cleaner and prevents the inevitable if (typeof item === "string") ladder.

2. Floating point precision. 0.1 + 0.2 !== 0.3 in JavaScript. JSON encodes floats the same way JavaScript does — with the same precision issues. For money, use strings ("19.99") or integers (1999 cents). Never use floats for currency.

3. Large numbers. JavaScript's Number type is a 64-bit float, but it can only safely represent integers up to 2^53 - 1. JSON has no such limitation, but a JavaScript client parsing your JSON will silently lose precision for larger numbers. If you need to send large integers (database IDs, timestamps in nanoseconds), encode them as strings.

4. Encoding issues. JSON requires UTF-8. If your data comes from a system that uses a different encoding (ISO-8859-1, Windows-1252), convert before serializing. The \" escape works the same regardless of source encoding, but the raw bytes don't.

5. Comments in JSON files. Strict JSON doesn't allow comments. If you need to annotate a JSON file with comments, you're using the wrong format. Use JSON5 (with comments), TOML, YAML, or move the comments to a sidecar markdown file. The temptation to add // TODO to a JSON config is strong; resist it.

Tools That Catch Problems Early

Every team I've worked on that takes JSON seriously uses some combination of:

Try the Formatter

Our JSON Formatter validates, pretty-prints, and minifies JSON in your browser. It's useful for ad-hoc validation when you have a JSON file that won't parse and you want to see exactly where the error is. Everything runs client-side, so your data stays on your machine — useful if you're formatting a JSON file that contains API keys or other secrets.

Paste your JSON, get it formatted or minified, see the syntax error if there is one. No login, no upload, no data leaves your browser.

Performance: What Actually Matters

JSON parsing speed is rarely the bottleneck in modern applications, but it can matter for large payloads. A few rules of thumb from real performance work:

Native JSON.parse is fast. V8's JSON parser can process 100MB+ per second on a modern CPU. If you're seeing slow JSON parsing, the bottleneck is usually the surrounding code (network, DOM updates, query strings), not the parse itself.

Streaming JSON for large files. If you're processing JSON files larger than 100MB, don't load them all into memory. Use a streaming parser (like stream-json in Node.js or ijson in Python) to process the data incrementally. Most web apps don't need this, but ETL pipelines and log analyzers do.

Binary formats for very large data. If you're sending structured data over the network and JSON's verbosity is a problem, consider MessagePack, BSON, or Protocol Buffers. These binary formats are typically 2-5x smaller than equivalent JSON and parse 2-3x faster. The trade-off is loss of human readability and additional tooling complexity.

Don't pretty-print in production. I covered this earlier but it bears repeating: pretty-printing JSON in production responses is a waste of bandwidth and CPU. Your CDN will gzip either form equally well, so if you're not serving compressed, fix that before worrying about whitespace.

Validate once, parse once. If you need to both validate and parse JSON (which is most public APIs), use a library that does both in a single pass. Doing them separately means walking the JSON tree twice, which doubles the parse cost. Ajv in JavaScript and orjson in Python both support this pattern.

The Future of JSON

JSON has been the lingua franca of web APIs for 25 years. It's not going anywhere soon, but it's also not the only option anymore. A few things to watch:

JSON Lines (JSONL). Newline-delimited JSON, one object per line. Common in log files and streaming data. Tools like jq handle it natively. If you're processing large log files, JSONL is much friendlier than wrapping multiple objects in an array.

JSON Schema 2020-12. The latest version of JSON Schema adds better support for conditional validation, custom formats, and recursive schemas. If you're writing a new schema, use this version — it's what every modern validator supports.

CBOR and BSON. Binary formats that preserve JSON's data model but compress it for efficiency. IoT devices and mobile apps often use these. Not relevant for most web apps, but worth knowing about.

For now, JSON is the right choice. It's simple, well-understood, universally supported, and human-readable. Don't overthink it. Write clean JSON, validate it, minify for production, pretty-print for development, and move on to the actual problem you're solving.

← Back to all posts