Когда API возвращает данные, клиент ожидает предсказуемую структуру: понятные имена полей, читаемые даты, чёткие правила про отсутствующие значения. Без договорённостей каждая команда изобретает свой формат — и интеграция превращается в квест угадать, что значит null в этом поле.
В этой статье разберём конкретные правила: как называть поля, как отдавать даты, когда возвращать 201, а когда 204, и почему null в ответе — это проблема.
Имена полей: camelCase, а не snake_case
В вебе сложились два лагеря: одни API используют created_at и order_id, другие — createdAt и orderId. Для JSON-API на Java правильный выбор — camelCase, потому что:
- JavaScript (главный потребитель REST API) использует camelCase по умолчанию;
- Jackson (стандартная JSON-библиотека в Spring) по умолчанию пишет имена полей как в Java — а Java использует camelCase.
Хороший пример JSON-ответа:
{
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"totalAmount": 1500.00,
"status": "IN_PROGRESS",
"createdAt": "2026-05-26T10:30:00Z",
"deliveryAddress": {
"streetName": "Ленина",
"zipCode": "123456"
},
"items": [
{ "itemId": "abc123", "productName": "Клавиатура", "quantity": 2 }
]
}
Несколько правил по именованию:
- Идентификаторы — с суффиксом
Id:orderId,customerId,parentCategoryId. Простоidнеочевидно — идентификатор чего? - Даты — строка в формате ISO 8601:
2026-05-26для даты,2026-05-26T10:30:00Zдля момента времени (сZдля UTC). Никаких2026-05-26 10:30:00без буквыT— это не стандарт. - Коллекции — во множественном числе:
items,tags,errors. - Enum-значения —
UPPER_SNAKE_CASE:IN_PROGRESS,CREDIT_CARD,OUT_OF_STOCK. Клиент сразу видит, что это перечисление.
Boolean-поля
Нет жёсткого требования писать isActive или active — оба варианта встречаются. Важно одно: единообразие в проекте. Если выбрали active/enabled — так везде; если isActive/isEnabled — тоже везде.
{
"active": true,
"hasDiscount": false,
"canCancel": true
}
Даты и время: ISO 8601
Всегда используйте ISO 8601. Это международный стандарт, который понимают все библиотеки во всех языках.
- Дата:
"2026-05-26" - Дата и время (UTC):
"2026-05-26T10:30:00Z" - Дата и время с офсетом:
"2026-05-26T13:30:00+03:00"
Частая ошибка — передавать Unix timestamp как число (1716720600). Это машиночитаемо, но неудобно при отладке и не очевидно клиенту. Строка ISO 8601 читается человеком и при этом одинаково хорошо парсится.
Формат ответа зависит от операции
Разные операции — разные коды ответа и разная структура тела.
Создание (POST) — 201 + Location + тело
Когда ресурс создан, сервер возвращает статус 201 Created и заголовок Location со ссылкой на созданный ресурс. В теле — сам созданный объект:
HTTP/1.1 201 Created
Location: /api/v1/orders/550e8400-e29b-41d4-a716-446655440000
{
"orderId": "550e8400-e29b-41d4-a716-446655440000",
"status": "CREATED",
"totalAmount": 0,
"createdAt": "2026-05-26T10:30:00Z"
}
Зачем Location? Клиент сразу знает URL нового ресурса, не нужно дополнительно угадывать или конструировать.
Обновление (PUT/PATCH) — 200 + обновлённый ресурс
После обновления возвращаем актуальное состояние объекта:
HTTP/1.1 200 OK
{
"orderId": "550e8400-...",
"status": "CONFIRMED",
"totalAmount": 1500.00,
"updatedAt": "2026-05-26T11:00:00Z"
}
Клиент сразу видит, что изменилось — без дополнительного GET-запроса.
Удаление (DELETE) — 204 No Content
Удаление ничего не возвращает. Статус 204 No Content, тело пустое:
HTTP/1.1 204 No Content
Не нужно отдавать { "success": true } — статус 204 уже говорит об успехе.
Действие над ресурсом (action) — 200 + результат
Если endpoint — это действие (подтвердить заказ, заблокировать пользователя), возвращаем обновлённый ресурс:
POST /api/v1/orders/550e8400-.../confirm
HTTP/1.1 200 OK
{
"orderId": "550e8400-...",
"status": "CONFIRMED",
"confirmedAt": "2026-05-26T11:00:00Z"
}
Единичный ресурс — плоский объект
Никаких обёрток. Просто объект:
{
"orderId": "550e8400-...",
"status": "CONFIRMED",
"totalAmount": 1500.00,
"createdAt": "2026-05-26T10:30:00Z"
}
Вложенные объекты и массивы внутри ресурса — нормально. Но не заворачивайте ресурс в { "data": ..., "success": true } — это антипаттерн, о нём ниже.
Коллекция — content + метаданные пагинации
Когда возвращаете список с пагинацией, структура такая:
{
"content": [
{ "orderId": "..." },
{ "orderId": "..." }
],
"page": 1,
"size": 20,
"totalElements": 243,
"totalPages": 13
}
Поле content — это сами данные, рядом — метаданные пагинации. Это не «обёртка» в плохом смысле, это структура страницы.
null в ответе — почему это проблема
Когда клиент получает null в поле, он не знает что это значит: данных нет совсем? Ещё не загружено? Было, но удалили? Каждое значение null — это неопределённость.
Правило простое: если поля нет — его не должно быть в JSON совсем, а не "discount": null.
Плохо:
{
"orderId": "...",
"discount": null,
"comment": null
}
Хорошо:
{
"orderId": "...",
"status": "CONFIRMED"
}
Ещё плюсы: меньше трафик, клиентский код проще (if (data.discount) вместо if (data.discount !== null && data.discount !== undefined)).
В Spring это настраивается одной строкой — говорим Jackson не включать null-поля:
@Bean
public ObjectMapper objectMapper() {
return Jackson2ObjectMapperBuilder.json()
.serializationInclusion(JsonInclude.Include.NON_NULL)
.build();
}
Та же логика про пустые строки: "" — это не «нет данных», это «есть, но пусто». Если данных нет — поля нет в JSON.
null в теле PATCH-запроса — другая история
В PATCH-запросах null имеет особый смысл согласно стандарту JSON Merge Patch (RFC 7396): это команда удалить поле.
PATCH /api/v1/orders/550e8400-...
Content-Type: application/merge-patch+json
{ "comment": null }
Это говорит: «убери поле comment из ресурса». Это семантика запроса, не нарушение правила про null в ответах.
Envelope — антипаттерн
Иногда видят такой формат ответа:
{
"success": true,
"data": {
"orderId": "...",
"status": "CREATED"
},
"error": null
}
Это называется envelope («обёртка»). Кажется удобным — всегда одинаковая структура. Но на практике это лишний слой без пользы:
- HTTP-статус (200, 404, 500) уже сообщает, успех или ошибка — дублировать его в
"success": trueнезачем. - Для ошибок есть отдельный стандарт (RFC 9457), который лучше описывает проблему.
- Клиент пишет
response.data.orderIdвместоresponse.orderId— лишний уровень.
Правильно: единичный ресурс возвращается плоским объектом, коллекция — через { "content": [...] } с пагинацией.
Пустые коллекции — [] а не null
Если коллекция пуста — возвращаем пустой массив, не null и не отсутствие поля:
{ "items": [] }
[] означает «коллекция есть, элементов нет». null или отсутствие поля — двусмысленно: то ли коллекция пуста, то ли её нет, то ли не загружена.
Коротко
- Имена полей — camelCase:
orderId,totalAmount,createdAt. - Идентификаторы — суффикс
Id:orderId,customerId. - Даты — строка ISO 8601:
2026-05-26T10:30:00Z. - Enum — UPPER_SNAKE_CASE:
IN_PROGRESS,CREDIT_CARD. - Создание — 201 Created + заголовок
Location+ тело ресурса. - Обновление — 200 OK + обновлённый ресурс.
- Удаление — 204 No Content, тело пустое.
- Единичный ресурс — плоский объект, без обёрток.
- Коллекция с пагинацией —
{ "content": [...], "page": ..., "totalElements": ... }. nullв ответе 2xx запрещён — если данных нет, поля нет в JSON.nullв теле PATCH — особая команда «удалить поле» (JSON Merge Patch).- Пустые коллекции —
[], неnull.
Что почитать дальше
- URL и ресурсы в REST API — HTTP-методы, статусы, структура URL.
- Query-параметры и пагинация — как передавать фильтры и получать страницы.
- Ошибки и проблемный ответ — формат ошибки RFC 9457.
- Заголовки и трассировка — Location, ETag и служебные заголовки.