JSON Formatting Best Practices for APIs and Config Files
Naming conventions, date formats, null handling and the structural choices that make JSON easy to consume — plus what the spec does and does not allow.
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.
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.
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
- Format and validate in one step with the JSON Formatter or check syntax with the JSON Validator.