← назад к разделу

Когда 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 и служебные заголовки.