1. Імена полів JSON як контракт
Коли ми починаємо проєктувати JSON, дуже легко скотитися в настрій: «Та яка різниця, як назвати поле, головне — дані ж правильні». Але в бекенд-світі це приблизно як сказати: «Яка різниця, як підписати дроти в щитку, головне — електрика ж є». Поки нічого не треба лагодити, здається, що різниці майже немає. А потім настає момент: «Чому в нас іскрить лише по пʼятницях?»
Якщо говорити строго, імʼя поля в JSON — це частина API-контракту. Клієнт (Postman, фронтенд, інший застосунок, навіть ваш майбутній Java-клієнт) читає не ваші думки, а конкретні рядки: externalId, status, items. Якщо ви перейменували externalId на external_id, клієнт не «здогадається», що це те саме. Він просто не знайде поле. І не тому, що клієнт тупий, а тому, що контракт — це саме домовленість у символах.
Можна уявити це так:
flowchart LR
Client["Клієнт (зовнішня сторона)"] -->|"JSON із полями"| Server["Сервер/застосунок"]
Server -->|"JSON із полями"| 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
Коли ви вперше робите кінцеву точку «отримати список», є спокуса повернути просто масив. Мовляв, «навіщо ускладнювати». Іноді це справді нормально. Але щойно з’являється потреба повернути поруч із масивом хоч щось іще (наприклад, count), починається питання: «А куди тепер це причепити?».
Тому ми в проєкті заздалегідь звикаємо до простої форми: обгортка списку. І тут знову важливі імена: найчастіше ви зустрінете items (елементи) і count (їхню кількість). Так, звучить банально. І це добре: банальне = передбачуване.
Хороша відповідь для списку читання:
{
"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
}
Чи можна так робити? Можна. Але тоді це теж має стати вашим узгодженням. Проблема в тому, що новачки часто роблять так: одна кінцева точка повертає 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 чого саме? якого endpoint?
Хороший приклад (трохи довший, але зате зрозумілий):
class CreateReadingItemRequest { } // вхід на створення
class UpdateReadingItemRequest { } // вхід на оновлення
class UpdateStatusRequest { } // вхід на зміну статусу (окремий сценарій)
class ReadingItemResponse { } // вихід: один елемент
class ReadingListResponse { } // вихід: колекція/список
Зверніть увагу: ми тут не граємо в чисту архітектуру і не будуємо десять рівнів абстракцій. Ми просто робимо так, щоб людина, відкривши файл, одразу розуміла: це вхід чи вихід, і для якої дії.
І ще одна тонкість: якщо два DTO збігаються за полями, це не означає, що їх треба зливати в один. Збіг полів — випадковість, а призначення — це сенс. Наприклад, CreateReadingItemRequest і UpdateReadingItemRequest можуть бути однаковими за набором полів, але семантично це різні операції. Тому навіть однакова форма не скасовує різні назви.
6. Перейменування полів і несумісна зміна
Перейменування поля — одна з найпідступніших правок у бекенд-світі. Усередині коду ви звикли: «Ну перейменував змінну — IDE все оновила, тести пройшли, живемо». Але JSON-контракт — це не «всередині проєкту». Це назовні. IDE клієнта ваших перейменувань не бачить. І якщо контракт уже використовує хтось іще, перейменування — майже завжди несумісна зміна.
Щоб не перетворювати лекцію на курс про версіонування API (це окрема історія), тримаємо просту думку: якщо поле вже опубліковане в контракті й клієнт на нього розраховує, то «просто перейменувати» не можна без наслідків.
Корисно побачити це на прикладі. Уявімо, що спочатку ми видавали відповідь так:
{
"externalId": "OL12345M",
"title": "Clean Code"
}
А потім хтось вирішив «зробимо красиво, як у Python» і змінив на:
{
"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-контракт так не працює. Якщо поле вже «вийшло назовні», будь-яке перейменування може зламати клієнтів. Тому перш ніж змінювати імʼя, поставте собі запитання: «Хто це вже використовує і що в них станеться?»
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ