JSON Formatting Best Practices: Syntax Rules and Common Errors
Most JSON problems fall into two groups, and only one of them is easy.
The first group is invalid JSON. A trailing comma, a single quote, a comment left in a config file. The parser refuses, you get a line number, you fix it. Annoying but self-announcing.
The second group is JSON that parses perfectly and is still wrong. Duplicate keys where one silently wins. An integer that arrives as a different number than it left as. A value that survives one language and changes meaning in another. Nothing errors. The data is simply not what you think it is.
This guide covers both, with the actual error text for the first and worked demonstrations for the second.
What JSON actually allows
JSON has exactly six value types. Everything you can express is built from these:
| Type | Example |
|---|---|
| String | `"hello"` |
| Number | `42`, `-3.14`, `1.2e10` |
| Object | `{"key": "value"}` |
| Array | `[1, 2, 3]` |
| Boolean | `true`, `false` |
| Null | `null` |
Notice what is absent. There is no date type, no integer type distinct from float, no binary type, no comment syntax, and no undefined. Dates are conventionally sent as ISO 8601 strings. Binary data is base64 encoded, as covered in our guide on what base64 encoding is and is not.
One point that catches people out: a bare value is valid JSON on its own. Under RFC 8259, "hello", 42, and null are each a complete, valid JSON document. The top level does not have to be an object or an array, though in practice most APIs return one.
The syntax rules parsers enforce
These are the rules that produce an error. Each of the following was run through a real parser, with the actual message:
| What you wrote | What the parser says |
|---|---|
| `{"a":1,}` | Expecting property name enclosed in double quotes |
| `[1,2,3,]` | Expecting value |
| `{'a':1}` | Expecting property name enclosed in double quotes |
| `{a:1}` | Expecting property name enclosed in double quotes |
| `{"a":1} // note` | Extra data |
| `{"a":007}` | Expecting ',' delimiter |
| `{"a":0xFF}` | Expecting ',' delimiter |
| `{"a":+5}` | Expecting value |
| `{"a":.5}` | Expecting value |
| `{"a":5.}` | Expecting ',' delimiter |
| `{"a":undefined}` | Expecting value |
Reading those messages is a skill in itself, because they rarely describe the actual mistake. "Expecting property name enclosed in double quotes" on a trailing comma sounds unrelated until you realise the parser consumed the comma, expected another key, and found a closing brace instead. The reported position is where the parser gave up, not where you went wrong. The real error is usually just before it.
Four rules cover almost all of the above:
Keys are double-quoted strings, always. Not single quotes, not bare words. JavaScript object literals allow both; JSON allows neither.
No trailing commas. The last element in an object or array is followed by nothing.
No comments. JSON has no comment syntax at all. If you need one, add a "_comment" key, or use a format that supports them for config files.
Numbers follow strict rules. No leading zeros, no hexadecimal, no leading plus, and a decimal point needs digits on both sides. 0.5 is valid, .5 and 5. are not.
One more that surprises people: a literal newline inside a string is invalid. It has to be escaped as \n. Pasting multi-line text straight into a JSON string is a common way to produce an "Invalid control character" error.
→ Use our free JSON Formatter at GlobalUtilityHub to validate, pretty-print, and minify JSON in your browser. No sign-up needed.
Strings, escaping, and Unicode
Strings are where JSON gets fiddly, and where hand-edited files most often break.
Seven characters must be escaped inside a JSON string:
| Character | Escaped as |
|---|---|
| `"` double quote | `\"` |
| `\` backslash | `\\` |
| newline | `\n` |
| tab | `\t` |
| carriage return | `\r` |
| backspace | `\b` |
| form feed | `\f` |
Any other control character below U+0020 must be written as a \u escape. This is why pasting multi-line text directly into a JSON string produces "Invalid control character": the literal newline needs to become \n.
Forward slash is the odd one. \/ is permitted but not required, so "a/b" and "a\/b" are both valid and produce the identical string. The escape exists so JSON can be embedded inside HTML <script> tags without the sequence </ terminating the element early. If a serialiser is producing \/ and you were not expecting it, that is why.
Unicode has two equivalent forms. JSON is UTF-8 by default under RFC 8259, so you can write characters directly, or as \u escapes. "café" and "caf\u00e9" parse to exactly the same string.
Which you emit is usually a serialiser setting. Python's json.dumps escapes non-ASCII by default, turning café into "caf\u00e9". Pass ensure_ascii=False and you get "café" instead. Both are correct; the escaped form is larger, 11 bytes against 7 in this case, but survives systems that mishandle UTF-8.
Characters outside the Basic Multilingual Plane need surrogate pairs when escaped. An emoji such as 🎉 is a single character, but written as escapes it becomes "\ud83c\udf89", two \u sequences representing one character. This matters if you are counting string length, slicing strings, or enforcing a character limit, because a naive length check will see two units where a user sees one.
Formatting conventions that actually matter
Once your JSON is valid, formatting is about who reads it next.
Two-space indentation for anything a human will read. Config files, fixtures, API documentation examples, anything committed to a repository. Two spaces over four is the dominant convention in JSON specifically, largely because nesting depth grows quickly and four-space indentation pushes deep structures off the screen.
Minify anything sent over a network. Whitespace is meaningless to a parser and it is not free. A small object of four keys measures 59 bytes minified and 89 bytes with two-space indentation, a 51% increase. On an API returning thousands of records that difference is real, and gzip does not make it disappear entirely.
Pick one key naming convention and hold it. camelCase, snake_case, and kebab-case are all valid. Which you choose matters far less than choosing once. Mixed conventions inside a single payload are a reliable source of bugs, because consumers end up writing lookups for both.
Sort keys when output is compared. If JSON gets diffed in code review or checked into version control, sorting keys alphabetically on serialisation means a diff shows what changed rather than what moved. Most serialisers support this directly.
Prefer flat structures where you have the choice. Deep nesting is valid and often unavoidable when it reflects real structure, but four levels of nesting to express a two-level relationship makes every consumer write four levels of access code.
Three things that parse and still break
This is the group that costs real time, because nothing errors.
Duplicate keys. {"a":1,"a":2} is accepted by essentially every parser. RFC 8259 says key names *should* be unique but does not require it, and leaves the behaviour undefined when they are not. In practice most implementations take the last occurrence, so that object parses to {"a": 2} and the first value vanishes. Different implementations are free to choose differently. If you are merging objects programmatically, this is where data disappears silently.
Large integers. JSON numbers have no defined size limit, but the languages consuming them do. JavaScript represents all numbers as 64-bit floats, giving a maximum safely representable integer of 9007199254740991.
Send {"id": 9007199254740993} to a JavaScript client and it parses without complaint as 9007199254740992. Off by one, silently, and it round-trips back out at the wrong value. Python has arbitrary-precision integers and parses the same input exactly, so the same payload is correct in one language and corrupted in another.
Large identifiers, such as database primary keys or Twitter-style snowflake IDs, should be sent as strings for exactly this reason.
NaN and Infinity. Neither is valid JSON. Both appear in real payloads anyway, because language implementations disagree about them.
Python's json module accepts NaN on parse by default and emits it on serialisation, producing output that is not valid JSON. JavaScript's JSON.parse rejects it with a SyntaxError, while JSON.stringify silently converts NaN to null.
So a Python service can emit something a JavaScript client cannot read, and a JavaScript service can turn a meaningful NaN into an indistinguishable null. In Python, pass allow_nan=False to make serialisation raise rather than emit invalid output.
Two related JavaScript behaviours worth knowing. JSON.stringify drops object keys whose value is undefined entirely, so {a: undefined, b: 1} serialises to {"b":1} with no warning. And floating point arithmetic carries through: 0.1 + 0.2 serialises as 0.30000000000000004, which is correct IEEE 754 behaviour and still surprising in a payload.
JSON against the alternatives
| Format | Comments | Trailing commas | Human editing | Typical use |
|---|---|---|---|---|
| JSON | No | No | Fair | APIs, data interchange |
| YAML | Yes | N/A | Good | Configuration, CI pipelines |
| TOML | Yes | Yes | Good | Application config |
| XML | Yes | N/A | Poor | Legacy systems, documents |
| CSV | No | N/A | Good | Flat tabular data |
The practical division: JSON for machine-to-machine interchange, YAML or TOML for files humans edit by hand. The absence of comments is what makes JSON awkward as a configuration format, which is why tsconfig.json and similar files use JSONC, a superset that permits comments and is not valid JSON.
CSV remains the right answer for genuinely flat tabular data, and converting between the two is common enough that we cover it separately in converting JSON to CSV in Python.
Working with JSON in practice
Validation belongs at the boundary. Parse and validate as data enters your system rather than discovering a malformed field four functions deep. For anything beyond simple checks, JSON Schema lets you describe required keys, types, and value constraints declaratively.
For language-specific work, we cover the common tasks in detail:
The bottom line
Valid JSON is a short list of rules: six value types, double-quoted keys, no trailing commas, no comments, strict number formatting. Parsers enforce all of it and tell you when you get it wrong, even if the message points slightly past the actual mistake.
The rules worth internalising are the ones no parser enforces. Keep keys unique. Send large identifiers as strings. Never let NaN into a payload. Those three account for a disproportionate share of the JSON bugs that take a full afternoon to find, precisely because nothing anywhere reports an error.
Our JSON Formatter validates, pretty-prints, and minifies in your browser in under 30 seconds. Try it free at globalutilityhub.com/dev-tools/json-formatter/
Use our free JSON Formatter to apply what you have learned.
Open JSON Formatter →