JSON Best Practices

Naming conventions, dates, null vs omit, large numbers, pagination, error responses, and nesting — the conventions that save debugging time.

7 min read·Updated June 2026

JSON is simple by design, but it's easy to make choices that create friction later — inconsistent naming, ambiguous nulls, brittle date strings. This guide collects the conventions that save the most debugging time. For a foundations refresher, see What is JSON?

Naming conventions

Use camelCase for keys. It is the dominant convention in JSON APIs, matches JavaScript's native style, and most JSON serializers in other languages (Jackson, Newtonsoft.Json, encoding/json with struct tags) map to it automatically.

Avoid

{
  "first_name": "Alice",
  "last-name": "Martin",
  "UserEmail": "alice@example.com"
}

Prefer

{
  "firstName": "Alice",
  "lastName": "Martin",
  "userEmail": "alice@example.com"
}

Use snake_case if your primary consumers are Python or PostgreSQL (where snake_case is idiomatic). What matters most is consistency — pick one and stick to it across the entire API.

Dates and times

Always use ISO 8601. Never invent custom date formats — "01/06/2026" is ambiguous (January 6th or June 1st?), locale-dependent, and unparseable by standard libraries.

Use caseFormatExample
Date onlyYYYY-MM-DD"2026-06-30"
Date + time (UTC)YYYY-MM-DDTHH:mm:ssZ"2026-06-30T14:30:00Z"
Date + time (with offset)YYYY-MM-DDTHH:mm:ss+HH:mm"2026-06-30T16:30:00+02:00"
Millisecond precisionYYYY-MM-DDTHH:mm:ss.SSSZ"2026-06-30T14:30:00.123Z"

Prefer UTC timestamps in APIs — consumers can convert to local time. Store epoch milliseconds as integers only if you need maximum compactness (e.g. high-volume event logs).

Null vs omitting a field

These are different semantics and should be used deliberately:

RepresentationSemanticsWhen to use
"middleName": nullThe field exists and is explicitly emptyWhen the consumer needs to distinguish "known to be absent" from "not provided"
(field omitted)The field is not part of this responseOptional fields that don't apply in this context; reduces payload size

Choose one approach and document it. Never mix them: an API that sometimes omits a field and sometimes returns null for it forces consumers to handle both cases.

Numbers

JSON numbers are conceptually arbitrary-precision, but in practice most parsers use IEEE 754 double-precision floats — which gives you exact integers up to 253.

  • Large IDs — if your IDs exceed 253 (e.g. Twitter/X snowflake IDs), send them as strings: "id": "1234567890123456789". JavaScript loses precision silently otherwise.
  • Money — never use floats for currency: 0.1 + 0.2 ≠ 0.3. Use integer cents/pence: "amountCents": 1099, or a string with a fixed decimal: "amount": "10.99".
  • Floats for display — round server-side before sending; don't push rounding responsibilities to clients.

Booleans

Use actual JSON booleans (true / false), never string substitutes:

Avoid

{
  "active": "true",
  "deleted": 1,
  "verified": "yes"
}

Prefer

{
  "active": true,
  "deleted": false,
  "verified": true
}

Pagination

Wrap paginated lists in an object — never return a bare array at the top level, because adding metadata later would break the API contract:

{
  "data": [
    { "id": "u_1", "name": "Alice" },
    { "id": "u_2", "name": "Bob" }
  ],
  "pagination": {
    "page": 1,
    "perPage": 20,
    "total": 142,
    "nextCursor": "eyJpZCI6InVfMiJ9"
  }
}

Cursor-based pagination (nextCursor) is more robust than page numbers for large or frequently-updated datasets — it doesn't skip or duplicate items when records are inserted.

Error responses

Return a consistent error shape regardless of HTTP status code. Consumer code can then handle errors generically:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request body is invalid",
    "details": [
      { "field": "email", "message": "Must be a valid email address" },
      { "field": "age",   "message": "Must be a positive integer" }
    ]
  }
}
  • code — machine-readable, stable string (not an HTTP status number).
  • message — human-readable, safe to display in developer tooling.
  • details — optional array of per-field errors for validation failures.

Nesting depth

Deep nesting is a sign the data model needs rethinking. Prefer flat structures where possible:

Avoid (deep nesting)

{
  "user": {
    "profile": {
      "address": {
        "city": {
          "name": "Paris"
        }
      }
    }
  }
}

Prefer (flat)

{
  "userId": "u_42",
  "cityName": "Paris",
  "addressLine1": "1 rue de Rivoli",
  "addressPostalCode": "75001"
}

Keep nesting to 2–3 levels max for most objects. Embed arrays for 1:many when the child list is always fetched with the parent; use IDs + separate endpoints when the child list is large or independently queried.

Versioning

Plan for evolution from day one. Additive changes (new fields) are generally safe — compliant clients ignore unknown keys. Breaking changes (removing, renaming, or retyping fields) require a new version.

  • URL versioning/api/v2/users — explicit and cacheable.
  • Header versioningAccept: application/vnd.api+json;version=2 — cleaner URLs but harder to test in browsers.
  • Always include a "version" or "schemaVersion" field in responses for APIs that embed schema info.

Validate your JSON with JSON Schema

Document your API shape as a JSON Schema and run validation on both sides (server: reject bad input; client: reject unexpected responses). This catches contract violations at the boundary, before bad data propagates into your system.

Frequently asked questions