JSON Best Practices

Updated: August 8, 2026

Naming, types, structure and compatibility — the decisions that determine whether consuming your JSON is straightforward or a permanent source of defensive code.

Consistency Beats Correctness

Most JSON design questions have several defensible answers and no single right one. camelCase or snake_case, envelope or bare resource, null or omitted — reasonable teams land in different places, and their APIs work fine.

What does not work is choosing differently in different places. A payload carrying both userId and created_at forces every consumer to remember which convention applies to which field, forever. The cost of inconsistency compounds; the cost of picking the less fashionable option is close to zero. Decide once, write it down, apply it everywhere.

Naming

{ "firstName": "Alice", "createdAt": "2026-08-07T14:30:00Z" }   // camelCase
{ "first_name": "Alice", "created_at": "2026-08-07T14:30:00Z" } // snake_case
  • Pick one case convention and hold it. camelCase is most common for public APIs; snake_case suits Python, Ruby and PostgreSQL stacks. Serialisers in every language can map between them, so your internal style need not leak.
  • Avoid abbreviations. organisationId reads better than orgId, and far better than oid. The bytes you save are recovered by compression anyway.
  • Prefix booleans predictably — isActive, hasSubscription, canEdit. A bare active reads ambiguously.
  • Use plural names for arrays. roles is an array; role is a single value. Getting this wrong makes every call site read incorrectly.
  • Never put data in key names. {"2026-08-07": 42} forces consumers to enumerate keys to find anything. Use [{"date": "2026-08-07", "value": 42}].

Choosing Types That Survive the Trip

JSON has seven types and you will constantly need things that are not among them. These conventions are settled — deviating from them creates work for every consumer.

Dates: ISO 8601, UTC, always

"createdAt": "2026-08-07T14:30:00Z"     // good
"createdAt": 1786112400                  // epoch — ambiguous units, unreadable
"createdAt": "07/08/2026"                // is that August 7th or July 8th?
"createdAt": "2026-08-07 14:30:00"       // no zone — ambiguous by hours

ISO 8601 sorts correctly as a plain string, parses everywhere, and states its time zone explicitly. A local time without an offset is ambiguous, and that ambiguity surfaces as an off-by-hours bug during a daylight-saving transition, months after the code was written.

Money: never a float

"price": "19.99"                          // string, parsed into a decimal type
"priceMinorUnits": 1999                   // integer cents, scale documented
"price": 19.99                            // a rounding error waiting to happen

Large IDs: strings

"id": "12345678901234567890"              // survives intact
"id": 12345678901234567890                // silently rounded by most parsers

Anything beyond 253 is not safely representable as a JSON number in most runtimes. Twitter shipped both id and id_str for years because of exactly this.

Keep the Shape Stable

The single most valuable property a payload can have is that its structure does not vary between responses. Every variation becomes a conditional in every client.

// Unstable — the type of "roles" depends on the data
{ "roles": "admin" }              // one role
{ "roles": ["admin", "editor"] }  // several

// Stable — always an array
{ "roles": ["admin"] }
{ "roles": [] }

Three rules follow from this:

  • A field's type never changes. If it is sometimes an array, it is always an array.
  • Empty collections are [] and {}, not null. This lets clients iterate unconditionally. Watch for Go's nil slice marshalling to null.
  • Optional fields are present with null rather than absent, so the key set is predictable — except in PATCH requests, where absent must mean "leave alone" and null must mean "clear".

Structure for the Consumer

// Too deep — mirrors your internal object graph
data.response.result.user.profile.contact.address.city

// Shaped for the caller
data.user.address.city

Every level of nesting is another null check on the client. Three or four levels is a reasonable ceiling. If your payload's depth reflects your service's class hierarchy rather than what the caller needs, flatten it.

Similarly, prefer arrays of objects over objects keyed by ID for anything on the wire — they preserve order, iterate cleanly, and can be described in a schema. Build lookup tables client-side if you need them.

Compatibility: Strict In, Tolerant Out

The rule that keeps APIs from breaking each other on deploy day, and it is directional.

Requests you receive: strict

Reject unknown fields. A client sending emial has a bug, and silently ignoring it stores an empty address while reporting success.

Responses you consume: tolerant

Ignore fields you do not recognise. Otherwise the provider adding a harmless new key breaks your client on their next release.

These changes are safe to make to a response; these are not:

Safe:      adding a new optional field
           adding a new value to an enum the client treats as opaque

Breaking:  removing a field
           renaming a field
           changing a field's type
           making an optional field required in a request

When you must break something, version it — a new endpoint path, or a version header. Silently changing a field's type is the most damaging option, because it fails at runtime in the consumer rather than at the boundary.

Security

  • Serialise explicitly, never implicitly. Dumping a database row or ORM entity straight to JSON is how password hashes and internal tokens end up in responses. List the fields you intend to expose.
  • Never build JSON by string concatenation. Use a serialiser. Hand-built JSON is to escaping what hand-built SQL is to injection.
  • Cap request body size. A deeply nested or enormous document can exhaust memory or stack before your validation ever runs.
  • Guard deep merges. Merging parsed input into existing objects without excluding __proto__ and constructor enables prototype pollution.
  • Keep internals out of error bodies. Stack traces and SQL fragments belong in your logs, not in a response. Return a correlation ID instead.
  • Validate before trusting. A document that parses is not a document that is safe. Enforce a schema at the boundary.

Formatting Conventions

  • Minify what crosses a network, format what humans read. Enabling gzip matters considerably more than either.
  • Keep committed files formatted. A minified file in version control produces one enormous changed line on every edit, making review impossible.
  • Sort keys in generated files. Deterministic output means a rebuild with no changes produces an empty diff.
  • Always UTF-8, never with a byte-order mark. A BOM breaks parsing at position 0 in many parsers.

Frequently Asked Questions

Should JSON keys be camelCase or snake_case?

Neither is more correct. camelCase is the most common choice for public HTTP APIs because JSON's heritage and most consumers are JavaScript; snake_case suits stacks that already use it throughout. What matters far more is picking one and applying it to every field in the API.

How should I represent dates in JSON?

ISO 8601 strings in UTC, such as "2026-08-07T14:30:00Z". JSON has no date type, and this format sorts correctly as a plain string, parses in every language, and is unambiguous about time zone — which local times without an offset are not.

Why should monetary values not be JSON numbers?

JSON numbers are usually decoded as 64-bit floats, and binary floating point cannot represent most decimal fractions exactly. Use a string like "19.99" parsed into a decimal type, or an integer count of minor units like 1999 cents with the scale documented.

Should optional fields be omitted or set to null?

Be consistent, and prefer always present with null over sometimes absent. A stable set of keys lets consumers access fields unconditionally and lets a schema describe the record precisely. The exception is PATCH requests, where absent and null must mean different things.

How deeply should JSON nest?

Three or four levels is a sensible ceiling for an API payload. Deeper structures force long access chains and a null check at every level. If your JSON mirrors an internal class hierarchy rather than the consumer's needs, it is usually too deep.

Related Resources

Related Resources