JavaRush /Курси /Java Server /Узгодження щодо іменування JSON

Узгодження щодо іменування JSON

Java Server
Рівень 11 , Лекція 3
Відкрита

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, тому що:

  1. так простіше узгодити його зі звичними Java-іменами полів у DTO і не плодити зайву ручну роботу з перекладом;
  2. це робить приклади зрозумілішими на старті, а ми й без того вивчаємо багато нового.

Ось приклад «нормальної» відповіді з одним елементом списку читання в цьому стилі:

{
  "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-контракт так не працює. Якщо поле вже «вийшло назовні», будь-яке перейменування може зламати клієнтів. Тому перш ніж змінювати імʼя, поставте собі запитання: «Хто це вже використовує і що в них станеться?»

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ