JSON Best Practices
Naming conventions, dates, null vs omit, large numbers, pagination, error responses, and nesting — the conventions that save debugging time.
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 case | Format | Example |
|---|---|---|
| Date only | YYYY-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 precision | YYYY-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:
| Representation | Semantics | When to use |
|---|---|---|
"middleName": null | The field exists and is explicitly empty | When the consumer needs to distinguish "known to be absent" from "not provided" |
| (field omitted) | The field is not part of this response | Optional 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 versioning —
Accept: 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.
Try it now