JSON Formatting Best Practices for APIs and Config Files
Naming, date formats, null handling and the structural choices that make JSON easy to consume — plus what the spec allows.
Table of contents
- What JSON actually permits
- Naming: pick one convention and never mix
- Dates: always ISO 8601 with an offset
- Null versus absent versus empty
- Structure: envelope or not
- Numbers: know the precision limit
- Formatting for humans versus machines
- Frequently asked questions
- Are duplicate keys valid JSON?
- Should I use JSON or YAML for config?
- How do I validate structure, not just syntax?
- Is BOM allowed at the start of a JSON file?
- Related reading
- References
JSON is a small specification with a lot of room for bad decisions. These are the conventions that consistently make a payload pleasant to consume.
What JSON actually permits#
Worth stating plainly, because most "invalid JSON" errors are one of these:
- Double quotes only.
'single'is not valid JSON. - Keys must be quoted.
{name: "x"}is a JavaScript object literal, not JSON. - No trailing commas.
[1, 2, 3,]is invalid. - No comments.
//and/* */are not part of the spec. - No
undefined,NaNorInfinity. Onlynullfor absence.
JSON5 and JSONC add comments and trailing commas, and tsconfig.json is JSONC — which is why it accepts comments while your API will not.
Naming: pick one convention and never mix#
camelCase is the pragmatic default for a web API, because it matches JavaScript and consumers do not have to transform keys. snake_case is the right choice if your primary consumers are Python or Ruby, or if it matches your database columns.
What matters more than which: consistency. A payload with userId, created_at and Email forces every consumer to write per-field mappings. The Case Converter moves a list of field names between conventions in one step, which is quicker than renaming them by hand and less error-prone than a find-and-replace.
Dates: always ISO 8601 with an offset#
{
"createdAt": "2026-06-09T14:30:00Z",
"badExample1": 1780000000,
"badExample2": "09/06/2026",
"badExample3": "2026-06-09 14:30:00"
}2026-06-09T14:30:00Z is unambiguous, sorts lexicographically in the same order as chronologically, and parses natively everywhere. The alternatives each fail on something: a Unix timestamp is unreadable and ambiguous between seconds and milliseconds; 09/06/2026 is June 9th or September 6th depending on the reader's country; a space-separated datetime with no zone is a guess.
Null versus absent versus empty#
These three are different statements and should not be used interchangeably:
{ "middleName": null, "note": "we know there is none" }
{ "note": "the field is absent — we do not know" }
{ "middleName": "", "note": "there is one and it is empty" }Decide a policy and document it. The common one for REST: omit fields you have no data for, use null for a known-empty value, and never use "" to mean absent.
For PATCH semantics this distinction becomes load-bearing — {"middleName": null} should clear the field, while omitting it should leave it alone. That is impossible to express if you conflate them.
Structure: envelope or not#
Two defensible shapes:
// Bare resource — simple, RESTful
{ "id": "1", "name": "Ada" }
// Envelope — room for metadata
{
"data": { "id": "1", "name": "Ada" },
"meta": { "requestId": "abc" }
}Bare is cleaner for single resources. An envelope earns its place for collections, where you need pagination metadata alongside the items:
{
"data": [/* items */],
"pagination": { "page": 1, "perPage": 20, "total": 143, "hasMore": true }
}The one thing to avoid is a top-level array for a collection. It leaves nowhere to add pagination later without a breaking change.
Numbers: know the precision limit#
JSON numbers are IEEE 754 doubles in every JavaScript parser, which means integers above 2^53 lose precision silently:
JSON.parse('{"id": 9007199254740993}').id; // 9007199254740992 ← wrongSnowflake IDs, some database bigints and financial values in minor units can all exceed this. Send them as strings:
{ "id": "9007199254740993", "amountCents": "12345678901234567" }Formatting for humans versus machines#
Two spaces of indentation for anything a person will read or diff; minified for anything on the wire. Note that gzip compresses repeated whitespace extremely well, so minifying a gzipped response saves only a few percent — the case for minifying is stronger for data stored uncompressed, like a database column or localStorage. The JSON Minifier reports both sizes, which is the number worth checking before assuming the saving is there.
Sorting keys alphabetically is lossless (JSON objects are unordered by definition) and makes diffs far more readable in version-controlled config.
Frequently asked questions#
Are duplicate keys valid JSON?#
The grammar allows them but the behaviour is unspecified. Every mainstream parser takes the last one, silently. Never rely on it.
Should I use JSON or YAML for config?#
YAML for anything humans hand-edit with comments — CI configs, Kubernetes manifests. JSON for machine-to-machine. See JSON vs YAML vs TOML.
How do I validate structure, not just syntax?#
JSON Schema. Syntax validation answers "is this parseable"; a schema answers "is this the shape I expect". See JSON Schema Validation.
Is BOM allowed at the start of a JSON file?#
RFC 8259 says no, but many parsers tolerate it. A leading BOM is a common cause of "unexpected token" on the very first character.
Related reading#
- Common JSON Errors and How to Fix Them
- JSON Schema Validation
- Does Minifying Still Matter With Brotli? — whether stripping whitespace still buys anything once compression is on
- Format and validate in one step with the JSON Formatter or check syntax with the JSON Validator.
References#
Tags
- JSON
- API
- Best Practices