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.
{
"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
}
users, orders, line_itemsis_active, has_children, can_editdescription not desc, quantity not qtyWrap your response in a consistent envelope.
{
"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
}
}
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Request body validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address",
"value": "not-an-email"
}
],
"request_id": "req_8f3k2j9s"
}
}
| 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 |
| 429 | RATE_LIMITED | Too many requests |
| 500 | INTERNAL_ERROR | Unexpected server error |
// 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"
}
}
// 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"
}
}
Use ISO 8601 with UTC timezone for all timestamps.
// GOOD
{
"created_at": "2025-05-01T10:30:00Z",
"scheduled_at": "2025-06-15T14:00:00+05:30",
"birth_date": "1990-07-22"
}
// BAD
{
"created_at": "May 1, 2025",
"created_at": 1746094200,
"created_at": "01/05/2025 10:30"
}
// Field present and null β value explicitly set to null
{ "name": "Alice", "middle_name": null }
// Field absent β doesn't apply
{ "name": "Alice" }
GET /v1/users
GET /v2/users
| 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 | Making a required field optional |
| Changing error codes | Adding new valid enum values |
// BAD
HTTP 200 OK
{ "success": false, "error": "User not found" }
// GOOD
HTTP 404 Not Found
{ "error": { "code": "NOT_FOUND", "message": "User with id usr_999 does not exist" } }
// BAD
{ "price_usd": 10.99, "price_eur": 9.50, "price_gbp": 8.75 }
// GOOD
{ "prices": [
{ "currency": "USD", "amount": 10.99 },
{ "currency": "EUR", "amount": 9.50 }
] }
| 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 |