1. Имена полей JSON как контракт
Когда мы начинаем проектировать JSON, очень легко скатиться в настроение “да какая разница, как назвать поле, главное — данные же правильные”. Но в backend‑мире это примерно как сказать: “какая разница, как подписать провода в щитке, главное — электричество же есть”. Пока не надо чинить, кажется, что правда всё равно. А потом наступает момент “почему у нас искрит только по пятницам”.
Если говорить строго, имя поля в JSON — это часть API‑контракта. Клиент (Postman, фронтенд, другое приложение, даже ваш будущий Java‑клиент) читает не ваши мысли, а конкретные строки: externalId, status, items. Если вы переименовали externalId в external_id, клиент не “догадается”, что это то же самое. Он просто не найдёт поле. И не потому, что клиент тупой, а потому что контракт — это именно договорённость в символах.
Можно представить это так:
flowchart LR
Client["Клиент (кто-то снаружи)"] -->|"JSON c полями"| Server["Сервер/приложение"]
Server -->|"JSON c полями"| Client
В этом обмене “магии понимания” нет. Есть только совпадение ожиданий. Поэтому мы и вводим соглашения об именах: чтобы ожидания были стабильными, а не зависели от вкуса автора “сегодня я люблю snake_case, а завтра — CamelCase и драму”.
2. Единый стиль: camelCase и snake_case
Почти у каждого новичка есть внутренний вопрос: “А как правильно — externalId или external_id?”. Секрет в том, что в индустрии встречается и то, и другое, и обе стороны могут быть “правильными” — в зависимости от команды, экосистемы, стандартов компании и исторических причин. Проблема начинается не там, где вы выбрали “не тот” стиль, а там, где вы выбрали два стиля одновременно.
Давайте очень приземлённо сравним варианты в таблице — без войн и обид:
| Стиль | Пример | Где чаще встречается | Типичная проблема |
|---|---|---|---|
| camelCase | externalId | Java/Spring экосистема, многие JSON API | Новички иногда пишут externalID (с двумя большими буквами) и получают “зоопарк” |
| snake_case | external_id | Python, часть публичных API, некоторые REST‑гайды | В Java‑коде поля обычно camelCase, и без явного правила легко начинается путаница |
| UPPER_CASE | STATUS | почти нигде как JSON‑контракт | выглядит как крик и быстро превращается в хаос |
Для нашего учебного проекта ReadLater Starter мы будем держать camelCase в JSON, потому что:
- так проще совпадать с привычными Java‑именами полей в DTO и не плодить лишнюю “ручную работу на перевод”;
- это делает примеры понятнее в начале пути (мы и так учим много нового).
Вот пример “нормального” ответа одного элемента списка чтения в этом стиле:
{
"id": 1,
"title": "Clean Code",
"author": "Robert C. Martin",
"status": "PLANNED",
"externalId": "OL12345M",
"comment": "Найти бумажное издание"
}
А вот пример того, как выглядит контракт, если стиль гуляет по полям как кот по клавиатуре:
{
"id": 1,
"bookTitle": "Clean Code",
"external_id": "OL12345M",
"STATUS": "PLANNED"
}
Технически это “тоже JSON”. Но контрактом это становится плохо: клиенту надо помнить три разных правила именования одновременно, и каждое новое поле будет “лотереей”.
3. Один смысл — одно имя: словарь ReadLater
Когда стиль (camelCase) выбран, следующая боль — синонимы. Новички часто называют одно и то же разными словами: bookId, externalId, catalogId, idFromProvider. Или state и status. Или list и items. А потом сами же не могут понять, где “настоящее поле”, а где “почти то же самое, но вроде другое”.
Чтобы этого избежать, полезно завести маленький словарь проекта: набор слов, которые мы используем стабильно. Это не бюрократия, а защита мозга от перегрева. Для ReadLater Starter можно договориться так:
| Смысл | Имя поля | Пример | Комментарий |
|---|---|---|---|
| Внутренний идентификатор элемента списка чтения | id | 42 | Появляется в response, обычно не нужен в create‑request |
| Идентификатор книги во внешнем каталоге | externalId | "OL12345M" | Это другая ось идентичности, не путать с id |
| Название книги | title | "Clean Code" | Не bookTitle и не name, если мы договорились про title |
| Автор | author | "Robert C. Martin" | В простом курсе достаточно строки |
| Статус чтения | status | "PLANNED" | Лучше одно слово, чем state, readingState, bookStatus вперемешку |
| Комментарий пользователя | comment | "Купить на бумаге" | Необязательное поле |
| Элементы списка в ответе коллекции | items | [ ... ] | Плюральное имя для массива |
| Количество элементов | count | 3 | Счётчик рядом со списком |
Смысл этой таблицы в том, что мы не пытаемся “придумать идеальные названия на века”. Мы просто выбираем один термин на один смысл и держим его. Это экономит время и нервы сильнее, чем кажется.
Теперь пример на Java‑стороне (пока без сериализации, просто форма DTO). Сравним хорошую и плохую ситуацию.
Плохая: в одном DTO “id”, в другом “bookId”, в третьем “externalID” — и всё про одно и то же.
class BadReadingItemResponse {
long id; // внутренний идентификатор элемента в нашем сервисе
String bookId; // непонятно: это внутренний id или внешний?
String externalID; // другое написание "внешнего id": ломает единый словарь
}
Хорошая: id — внутренний, externalId — внешний. И везде одинаково.
class ReadingItemResponse {
long id; // внутренний id элемента (генерируется нашим сервисом)
String externalId; // внешний идентификатор из каталога (это другой смысл, не путать с id)
String title; // название книги
}
Да, это “просто названия”. Но это те самые названия, которые потом будут в JSON и которые увидит любой клиент.
4. Имена списков: items и count
Когда вы впервые делаете endpoint “получить список”, есть соблазн вернуть просто массив. Типа “чего усложнять”. И иногда это действительно нормально. Но как только появляется необходимость вернуть рядом с массивом хоть что‑то ещё (например, count), начинается “а куда теперь это приклеить”.
Поэтому мы в проекте заранее привыкаем к простой форме: обёртка списка. И тут снова важны имена: чаще всего вы встретите items (элементы) и count (их количество). Да, звучит банально. И это хорошо: банально = предсказуемо.
Хороший ответ для списка reading list:
{
"items": [
{
"id": 1,
"title": "Clean Code",
"author": "Robert C. Martin",
"status": "PLANNED",
"externalId": "OL12345M",
"comment": null
}
],
"count": 1
}
Плохой ответ — не потому что “не по стандарту”, а потому что клиенту приходится гадать, что такое data, и почему рядом totalCount, а не count, и почему элементы называются list, а не items:
{
"data": [
{ "id": 1, "title": "Clean Code" }
],
"totalCount": 1
}
Можно ли так делать? Можно. Но тогда это тоже должно стать вашим соглашением. Проблема в том, что новички часто делают так: один endpoint возвращает items + count, другой data + totalCount, третий вообще “просто массив”, а четвёртый — result. И вот это уже не контракт, а квест.
На уровне DTO это выглядит очень просто:
class ReadingListResponse {
ReadingItemResponse[] items; // элементы списка (то, что клиент будет перебирать)
int count; // количество элементов (удобно для UI и пагинации)
}
Обратите внимание: даже в Java‑коде полезно держать те же имена, что и в JSON. Не потому что “так всегда”, а потому что в учебном проекте это снижает количество движущихся частей.
5. Имена DTO по ролям
Новички часто страдают от того, что “в проекте много классов, я путаюсь”. И это нормально: DTO действительно плодятся. Но путаница почти всегда не из‑за количества классов, а из‑за того, что их названия не подсказывают роль. Когда вы видите класс Data, вы не понимаете ничего. Когда вы видите CreateReadingItemRequest, мозг хотя бы понимает: “это вход на создание”.
Здесь полезно держать простое соглашение: в названии DTO должно быть видно направление и сценарий. Направление мы будем показывать суффиксами Request и Response. Сценарий — глаголом или контекстом: Create, Update, UpdateStatus, ReadingItem, ReadingList.
Плохой пример (вроде бы “коротко”, но непонятно):
class ItemDto { } // непонятно: это request или response? для какого сценария?
class ItemDto2 { } // "2" не объясняет смысл, это просто счётчик
class Payload { } // payload чего именно? какого эндпоинта?
Хороший пример (чуть длиннее, но зато яснее):
class CreateReadingItemRequest { } // вход на создание
class UpdateReadingItemRequest { } // вход на обновление
class UpdateStatusRequest { } // вход на изменение статуса (отдельный сценарий)
class ReadingItemResponse { } // выход: один элемент
class ReadingListResponse { } // выход: коллекция/список
Заметьте, мы здесь не “играем в чистую архитектуру” и не делаем десять уровней абстракций. Мы просто делаем так, чтобы человек, открыв файл, сразу понимал: это вход или выход, и для какого действия.
И ещё тонкость: если два DTO совпадают по полям, это не значит, что их надо склеить. Совпадение полей — случайность, а назначение — смысл. Например, CreateReadingItemRequest и UpdateReadingItemRequest могут быть одинаковыми по набору полей, но семантически это разные операции. Поэтому даже одинаковая форма не отменяет разные имена.
6. Переименование полей и breaking change
Переименование поля — одна из самых обманчивых правок в backend‑мире. Внутри кода вы привыкли: “ну переименовал переменную — IDE всё обновила, тесты прошли, живём”. Но JSON‑контракт — это не “внутри проекта”. Это наружу. IDE клиента ваши переименования не видит. И если контракт уже используется кем‑то ещё, переименование — почти всегда breaking change.
Чтобы не превращать лекцию в курс про версионирование API (это отдельная история), держим простую мысль: если поле уже опубликовано в контракте и клиент на него рассчитывает, то “просто переименовать” нельзя без последствий.
Полезно увидеть это на примере. Представим, что сначала мы выдавали ответ так:
{
"externalId": "OL12345M",
"title": "Clean Code"
}
А потом кто‑то решил “сделаем красиво, как в питоне” и поменял на:
{
"external_id": "OL12345M",
"title": "Clean Code"
}
С точки зрения сервера всё ок: данные есть. Но клиент, который искал externalId, теперь его не найдёт. В лучшем случае он покажет пустое поле. В худшем — упадёт с ошибкой, и пользователь скажет “ваш сервис сломан”. И формально он будет прав: контракт изменился.
Есть изменения, которые обычно менее болезненны. Например, добавить новое необязательное поле (которое клиент может игнорировать) обычно проще, чем переименовать существующее. Но мы пока держимся на базовом уровне: имена полей — это обещание, и менять обещание надо осторожно.
7. Типичные ошибки при именовании JSON
Ошибка №1: смешивание стилей именования в одном и том же проекте.
Самая частая история: часть DTO написали в camelCase, потом кто‑то добавил поле в стиле snake_case, потом в ответе внезапно появилось STATUS заглавными буквами, “потому что так заметнее”. В итоге клиент читает контракт как ребус. Правило простое: выбираем один стиль и держим его везде.
Ошибка №2: несколько названий для одного смысла (синонимы).
Сегодня вы называете внешний идентификатор externalId, завтра — catalogId, послезавтра — bookId. Каждый раз кажется, что “ну вроде понятно”. Но через неделю вы сами перестаёте понимать, это три разных поля или одно и то же. Спасает мини‑словарь проекта: один смысл — одно имя.
Ошибка №3: “универсальные” поля типа data, info, result, которые ничего не говорят клиенту.
Иногда кажется, что data — это удобно, ведь “там данные”. Но API‑контракт читают люди и программы. items говорит: “это элементы списка”. count говорит: “это количество”. data говорит: “ну… что‑то”. Чем конкретнее имя, тем меньше лишних вопросов у клиента.
Ошибка №4: случайные сокращения и странные аббревиатуры.
extId, authNm, ttl выглядят “компактно”, но читаются как шифр. В учебном проекте мы ценим ясность выше экономии двух символов. Если поле реально часто используется, вы всё равно будете его писать в коде много раз, и понятное имя окупается быстрее, чем кажется.
Ошибка №5: переименование полей “для красоты” без понимания последствий.
Внутри Java‑кода переименование обычно безопасно: IDE обновит ссылки. Внешний JSON‑контракт так не работает. Если поле уже “вышло наружу”, любое переименование может сломать клиентов. Поэтому прежде чем менять имя, задайте себе вопрос: “Кто это уже использует и что у них произойдёт?”
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ