JSON Best Practices

Practical conventions for designing JSON: key naming, handling dates and money, nulls versus missing fields, error shapes, and structuring API responses.

8 min read

Conventions, not rules

JSON's specification says nothing about how to design data — only how to write it. These are the conventions that hold up in production, and the mistakes that are expensive to reverse once an API has consumers.

Key naming

Pick one convention and apply it without exception:

  • camelCase — conventional for JavaScript and TypeScript APIs; no conversion needed client-side.
  • snake_case — conventional in Python, Ruby, and PostgreSQL ecosystems.

Neither is superior. What causes real harm is mixing them, which forces every consumer to remember which endpoint uses which.

A few rules that are not merely stylistic:

{
  "userId": 42,
  "isActive": true,
  "createdAt": "2026-07-29T10:45:00Z",
  "itemCount": 3
}
  • No spaces or hyphens in keys. Legal JSON, but they force bracket access (obj["user-id"]) in most languages, and break dot notation.
  • Do not start keys with a digit, for the same reason.
  • Prefix booleans with is, has, or canisActive reads unambiguously where active does not.
  • Plural keys for arraysroles, not role.
  • Avoid reserved-ish names such as type, class, and id where a more specific term exists.

Dates

Use ISO 8601 in UTC, always:

{ "createdAt": "2026-07-29T10:45:00Z" }

It is unambiguous, sorts correctly as a plain string — a genuinely useful property — is readable by humans, and is parsed natively everywhere.

Avoid Unix timestamps in public APIs: 1785062700 is ambiguous about seconds versus milliseconds, and unreadable in a log. Never use locale-dependent formats — 03/04/2026 is 3 April in London and 4 March in New York.

For a date with no time component, use "2026-07-29". If local time genuinely matters — a calendar event — send the offset ("2026-07-29T10:45:00+01:00") or a separate IANA timezone field ("Europe/London"), because an offset alone does not survive daylight-saving transitions.

Money

Never use a float. JSON numbers are IEEE 754 doubles, and 0.1 + 0.2 === 0.30000000000000004. Over enough transactions those errors accumulate into real discrepancies.

{ "amount": 1099, "currency": "GBP", "scale": 2 }
{ "amount": "10.99", "currency": "GBP" }

Integer minor units are the safest and are what payment processors use. A decimal string is acceptable when the consumer has a decimal type. Either way, always include an explicit currency code — a bare number is not a monetary amount.

Large integers

Integers above 2^53 lose precision when parsed as doubles:

JSON.parse('{"id": 9007199254740993}').id  // → 9007199254740992

64-bit database IDs and snowflake identifiers exceed this routinely. Send them as strings:

{ "id": "9007199254740993" }

Twitter/X hit this publicly and now returns both id and id_str. The failure is silent, which is what makes it dangerous.

null versus absent versus empty

Three states that are easy to confuse:

  • "manager": null — the field applies, the value is genuinely unknown or empty
  • "manager": "" — an empty string, which is a value, not an absence
  • key absent — the field does not apply to this record

Pick a convention, document it, and stick to it. A common one: use null for known-empty, omit the key for not-applicable, and never use "" to mean absent.

For collections, prefer [] over null — a client can iterate an empty array without a guard, which removes a whole class of null checks.

Response structure

Wrap collections in an object. A top-level array leaves nowhere to add pagination or metadata later without breaking every consumer:

{
  "data": [ { "id": 1 }, { "id": 2 } ],
  "pagination": { "page": 1, "perPage": 20, "total": 137 }
}

Keep the shape stable. A field should not be a string sometimes and an array other times. Static clients cannot handle it.

Do not nest more than three or four levels. Deep nesting is hard to read, hard to query, and usually means a flatter model or a separate endpoint is called for.

Use a consistent error shape across every endpoint:

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "The request could not be processed.",
    "details": [
      { "field": "email", "issue": "must be a valid email address" }
    ]
  }
}

A stable machine-readable code matters more than the human-readable message — clients branch on the code, and messages get rewritten.

Versioning

Additive changes are safe. Removing or renaming a field, or changing its type, is breaking. Plan for it before the first consumer arrives:

  • URL versioning/v1/users — most visible and easiest to reason about
  • Header versioningAccept: application/vnd.api+json;version=1 — cleaner URLs, less discoverable

Whichever you choose, treat unknown fields as non-breaking on the client. A consumer that rejects unfamiliar fields cannot survive any additive change — which is why Jackson's default FAIL_ON_UNKNOWN_PROPERTIES causes so much trouble when consuming third-party APIs.

Security

Never put secrets in JSON that reaches a client. It is plain text; Base64 is not encryption.

Do not produce duplicate keys. {"role": "user", "role": "admin"} is undefined behaviour — parsers disagree on which wins, and two systems in one pipeline resolving it differently has produced real authorisation bypasses.

Validate depth and size on input. Deeply nested JSON can exhaust the stack of a recursive parser; a size cap and a depth cap are cheap defences.

Escape JSON embedded in HTML. The sequence </script> inside a string terminates the surrounding tag. Escape the forward slash as \/ — permitted precisely for this reason.

Practical checks

  • Format files in the repository. Readable diffs are worth more than the bytes.
  • Validate in CI so malformed JSON never merges. See JSON Validation.
  • Publish a schema for anything crossing a boundary you do not control.
  • Compress in transit rather than minifying. See Minifying JSON.
  • Document your conventions — dates, nulls, naming, errors — in one place consumers can find.

Related

Frequently asked questions

Should JSON keys be camelCase or snake_case?

Either works; consistency matters far more than the choice. camelCase is conventional in JavaScript and TypeScript APIs and needs no conversion on the client. snake_case is conventional in Python, Ruby, and PostgreSQL ecosystems. Pick one, document it, and apply it everywhere — a mixed API forces every consumer to remember which convention each endpoint uses.

What is the best date format for JSON?

ISO 8601 in UTC: "2026-07-29T10:45:00Z". It is unambiguous, sorts correctly as a plain string, is human-readable, and is parsed natively by essentially every language. Avoid Unix timestamps in public APIs because they are ambiguous about seconds versus milliseconds, and avoid locale-dependent formats entirely — 03/04/2026 means different days on different continents.

How should I represent money in JSON?

As an integer in minor units (1099 for £10.99) or as a string ("10.99"), always alongside an explicit currency code. Never use a floating-point number: JSON numbers are IEEE 754 doubles, which cannot represent 0.1 exactly, so arithmetic accumulates rounding errors. This is a bug that surfaces in production accounting rather than in tests.

Should a missing value be null or an absent key?

Use null when the field exists and its value is genuinely unknown or empty. Omit the key when the field does not apply to this record at all. Then be consistent, and document which you use — the ambiguity between null, "", and absent is a common source of client bugs, because the three states are indistinguishable in minified JSON read by eye.

Should a JSON API return an array at the top level?

Prefer an object. A top-level array leaves nowhere to add pagination, metadata, or warnings later without a breaking change, and historically some browsers had a JSON-hijacking vulnerability with array roots. Wrap the collection: {"data": [...], "pagination": {...}} costs one level of nesting and keeps the response extensible.

More in JSON Guide

← Back to JSON Guide