Free online JSON formatter, validator, and developer tools.
A well-designed JSON API is a joy to consume. A poorly designed one creates bugs, confusion, and endless Slack threads asking "what does this field actually mean?" The decisions you make about naming, error formats, pagination, and date handling ripple through every client that ever calls your API. This guide covers the patterns that separate maintainable APIs from painful ones, with concrete JSON examples for each.
Pick snake_case or camelCase and be consistent across every endpoint. Most REST APIs use one of these. The worst choice is mixing them.
{
"user_id": 42,
"first_name": "Alice",
"last_login_at": "2025-05-01T10:30:00Z",
"is_active": true
}
{
"userId": 42,
"firstName": "Alice",
"lastLoginAt": "2025-05-01T10:30:00Z",
"isActive": true
}
Both are fine. The critical rule: never mix. A response body with user_id alongside firstName is a maintenance nightmare.
users, orders, line_itemsis_active, has_children, can_editdescription not desc, quantity not qtyNumber can't safely represent integers above 253 โ 1// IDs from distributed systems (Snowflake IDs, etc.) should be strings
{
"id": "7823648237462834762", // string โ safe in all languages
"order_number": 10045 // small integer โ fine as number
}
Wrap your response in a consistent envelope. Clients should be able to predict the shape of every response without reading the documentation.
{
"data": {
"id": "usr_42",
"name": "Alice",
"email": "alice@example.com",
"created_at": "2024-01-15T08:00:00Z"
}
}
{
"data": [
{ "id": "usr_42", "name": "Alice" },
{ "id": "usr_43", "name": "Bob" }
],
"meta": {
"total": 248,
"page": 1,
"per_page": 20
}
}
The data wrapper lets you add meta, links, or other top-level fields later without breaking existing clients. If you return a bare array at the top level, you can never add pagination metadata without a breaking change.
// BAD: bare array โ can never add metadata without breaking clients
[
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
]
// GOOD: wrapped โ extensible
{
"data": [
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
]
}
Error responses are where the most inconsistency lives. Some APIs return HTML error pages, some return plain strings, some return JSON with completely different keys per endpoint. Pick a format and enforce it via middleware so every error โ including 500s โ returns the same shape.
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Request body validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address",
"value": "not-an-email"
},
{
"field": "age",
"message": "Must be a positive integer",
"value": -5
}
],
"request_id": "req_8f3k2j9s"
}
}
Key fields explained:
code โ machine-readable string constant clients can switch on. Never change these once published.message โ human-readable, can change. For debugging, not UI display.details โ array of field-level errors for validation failures. Empty array or omitted for non-validation errors.request_id โ correlates the error to a server log entry. Invaluable for support.| HTTP Status | Error Code | When to Use |
|---|---|---|
| 400 | VALIDATION_FAILED | Request body or query params failed validation |
| 401 | UNAUTHORIZED | No valid authentication credentials |
| 403 | FORBIDDEN | Authenticated but not permitted |
| 404 | NOT_FOUND | Resource does not exist |
| 409 | CONFLICT | Optimistic lock failure, duplicate key |
| 422 | UNPROCESSABLE | Syntactically valid but semantically wrong |
| 429 | RATE_LIMITED | Too many requests |
| 500 | INTERNAL_ERROR | Unexpected server error |
Always pair the HTTP status code with a machine-readable code field. The status tells the client the category; the code tells it the specific cause.
Three pagination patterns exist. Each has tradeoffs.
// Request: GET /users?page=3&per_page=20
{
"data": [ ... ],
"meta": {
"total": 248,
"page": 3,
"per_page": 20,
"total_pages": 13
},
"links": {
"first": "/users?page=1&per_page=20",
"prev": "/users?page=2&per_page=20",
"next": "/users?page=4&per_page=20",
"last": "/users?page=13&per_page=20"
}
}
Pros: Can jump to any page, easy to implement.
Cons: Skips or duplicates records if the dataset changes mid-pagination. Slow on large tables (SQL OFFSET 10000 scans 10,000 rows).
// Request: GET /events?limit=20&after=cursor_abc123
{
"data": [ ... ],
"meta": {
"has_next_page": true,
"has_prev_page": true
},
"links": {
"next": "/events?limit=20&after=cursor_xyz789",
"prev": "/events?limit=20&before=cursor_abc123"
}
}
Pros: Stable โ new inserts don't cause duplicates or skips. Efficient โ keyset pagination uses an index.
Cons: Can't jump to page 7. Total count is expensive to compute.
Use cursor pagination for feeds, timelines, and any high-write collection. Use offset pagination for admin tables where users need to jump to specific pages.
Always include next and prev links so clients can paginate without constructing URLs themselves. This is the HATEOAS principle applied minimally and practically.
Use ISO 8601 with UTC timezone for all timestamps. Full stop.
// GOOD
{
"created_at": "2025-05-01T10:30:00Z", // UTC
"scheduled_at": "2025-06-15T14:00:00+05:30", // with offset if timezone matters
"birth_date": "1990-07-22" // date-only when time is irrelevant
}
// BAD
{
"created_at": "May 1, 2025", // ambiguous, not machine-parseable
"created_at": 1746094200, // Unix timestamp โ readable but not self-documenting
"created_at": "01/05/2025 10:30" // is that May 1 or January 5?
}
Rules:
2025-05-01T10:30:00 without a timezone is ambiguous.Z suffix for UTC. Use +HH:MM offset only when the local timezone is meaningful (e.g., a calendar event that should appear at 2 PM local time).YYYY-MM-DD) when time of day is genuinely not relevant (birth date, expiry date).{
"timeout_seconds": 30, // explicit unit in the field name
"retry_interval_ms": 500, // milliseconds when precision matters
"session_duration": "PT1H30M" // ISO 8601 duration format
}
Always encode the unit in the field name for numeric durations. timeout: 30 is seconds? milliseconds? minutes? You'll get it wrong and break something.
null and a missing field mean different things. Be explicit about which you use and why.
// Field present and null โ the value exists, it was explicitly set to null
{
"name": "Alice",
"middle_name": null
}
// Field absent โ you don't know or it doesn't apply
{
"name": "Alice"
// middle_name not included
}
The practical rule most APIs follow:
null when the field is part of the resource schema but has no value. Clients can rely on the key always being present.shipping_address on a digital product order).// BAD โ using empty string to mean "no value"
{
"company": ""
}
// GOOD โ null is explicit
{
"company": null
}
// GOOD โ omit if not applicable
{}
Empty string is a valid value โ it means a string of zero characters. It should never be used as a sentinel for "no value."
// Request: GET /users/42?fields=id,name,email
{
"data": {
"id": "usr_42",
"name": "Alice",
"email": "alice@example.com"
// other fields omitted by client request, not null
}
}
When supporting sparse fieldsets, document clearly that missing fields are excluded by the client, not null.
Plan for breaking changes from day one. The three most common approaches:
GET /v1/users
GET /v2/users
Simple, explicit, cacheable. The version is visible in every log entry and URL. The downside is that clients have to update URLs when migrating versions.
GET /users
Accept: application/vnd.myapi.v2+json
Keeps URLs clean but requires clients to set headers correctly and makes versioning invisible in browser address bars.
| Breaking | Non-breaking (safe to ship) |
|---|---|
| Removing a field | Adding a new optional field |
| Renaming a field | Adding a new endpoint |
| Changing a field's type | Adding a new HTTP method to an endpoint |
| Changing error codes | Making a required field optional |
| Changing auth requirements | Adding new valid enum values (controversial) |
// BAD โ client has to parse body to detect failure
HTTP 200 OK
{
"success": false,
"error": "User not found"
}
// GOOD โ HTTP status communicates outcome
HTTP 404 Not Found
{
"error": {
"code": "NOT_FOUND",
"message": "User with id usr_999 does not exist"
}
}
When you return 200 for errors, every client has to add custom error-detection logic. HTTP status codes are the contract โ use them.
// BAD โ some endpoints return numeric IDs, others return strings
GET /users/42 โ { "id": 42 }
GET /orders/42 โ { "id": "ord_42" }
// GOOD โ consistent ID format everywhere
GET /users/usr_42 โ { "id": "usr_42" }
GET /orders/ord_42 โ { "id": "ord_42" }
// BAD โ field names change with data; impossible to handle generically
{
"price_usd": 10.99,
"price_eur": 9.50,
"price_gbp": 8.75
}
// GOOD โ structure encodes the variation
{
"prices": [
{ "currency": "USD", "amount": 10.99 },
{ "currency": "EUR", "amount": 9.50 },
{ "currency": "GBP", "amount": 8.75 }
]
}
// BAD โ three levels deep to get to the data you want
{
"response": {
"body": {
"result": {
"users": [...]
}
}
}
}
// GOOD โ data at predictable depth
{
"data": [...]
}
// BAD โ sometimes returns object, sometimes array
GET /users/42 โ { "id": 42, "name": "Alice" }
GET /users โ { "id": 1, "name": "..." } // forgetting to wrap in array
// GOOD โ collections always arrays, resources always objects
GET /users/42 โ { "data": { "id": 42 } }
GET /users โ { "data": [ { "id": 42 } ] }
// BAD โ reveals DB column names, table structure, ORM internals
{
"tbl_usr_id": 42,
"__v": 0,
"_id": "507f1f77bcf86cd799439011",
"mongo_created": 1746094200
}
// GOOD โ API shape is independent of storage
{
"id": "usr_42",
"created_at": "2025-05-01T10:30:00Z"
}
| Concern | Recommendation |
|---|---|
| Field naming | snake_case or camelCase โ pick one and enforce it everywhere |
| Response shape | Wrap in data key; never bare arrays at top level |
| Error format | Consistent envelope: code, message, details, request_id |
| HTTP status codes | Use them correctly โ 200 means success |
| Pagination | Cursor for feeds; offset for admin; always include links |
| Dates | ISO 8601 with UTC (Z suffix) always |
| Null handling | null = known absence; omit = not applicable |
| IDs | Strings for large IDs; consistent prefix per resource type |
| Versioning | URL versioning for simplicity; plan for breaking changes |
Well-designed JSON APIs aren't just easier to consume โ they're easier to maintain. When the response shape is consistent, clients are more resilient, debugging is faster, and adding new features doesn't require coordinating SDK changes across every consumer.
Use the JSON Formatter Hub to format, validate, and fix your JSON right now โ free and fully browser-based.