1. Вкладений JSON — нормальна форма
Якщо ви звикли до навчальних прикладів із двох полів, то реальна відповідь API може виглядати як вкладна лялька: об’єкт, усередині масив, усередині ще один об’єкт, а поруч — «службові» поля. Це не через примху авторів API, а через бажання передати дані разом із контекстом: кількістю результатів, сторінкою, статусом операції.
Почнімо з простої людської причини: дані у світі рідко бувають пласкими. Книга має назву і рік, але ще у неї є автори (а це вже список), іноді — кілька ідентифікаторів (зовнішній ID, внутрішній ID), іноді — вкладений блок “publisher” або “links”. Коли API повертає це все однією відповіддю, він неминуче використовує вкладеність.
Друга причина — практична: API часто хоче повернути не лише «список об’єктів», а й підказку «скільки їх узагалі» (наприклад, count), «який запит ви зробили» (query), «час обробки» (tookMs). У результаті маємо об’єкт-обгортку: усередині — основні дані, поруч — метадані. Це як посилка: всередині — ваше замовлення, а зовні — наліпка «крихке», «вага», «номер відстеження». Вміст важливий, але й наліпка допомагає зрозуміти, що відбувається.
І так, вкладеність майже завжди з’являється в тих місцях, де є відношення: «результати пошуку → кожен результат → у результату автори → у автора імʼя». У майбутньому, коли ви дивитиметеся на реальні відповіді зовнішнього каталогу (через Postman), ви бачитимете цю вкладну структуру постійно. Наше завдання зараз — навчитися її спокійно розгортати, а не намагатися з’їсти цілком.
2. Верхній рівень документа
Вкладений JSON найпростіше читати, якщо ви завжди починаєте з одного й того ж запитання: що в нас на самісінькому верху — об’єкт чи масив? Це займає дві секунди, але економить десять хвилин паніки. Перший символ (після пробілів) майже завжди одразу відповідає: { означає об’єкт, [ — масив.
Це суперпросте правило, але воно реально працює як пасок безпеки. Бо якщо ви помилилися на верхньому рівні, то далі неправильно інтерпретуватимете все інше. Наприклад, ви очікуєте поле count, а зверху у вас масив — і ви будете шукати його в кожному рядку, як «Де тут кнопка “Зберегти”?» у мікрохвильовці.
Невелика таблиця, щоб закріпити сенс «верхнього символу»:
| Верхній рівень | Як виглядає | Що це означає | Типовий зміст в API |
|---|---|---|---|
| Об’єкт | |
Набір іменованих полів | Відповідь «про одну сутність» або «обгортка навколо списку + метадані» |
| Масив | |
Список значень | Відповідь «лише список, без метаданих» (трапляється, але рідше) |
Усередині API частіше трапляється саме об’єкт на верхньому рівні, бо він дає гнучкість: можна повернути items, count, requestId і не ламати контракт, якщо додадуться нові поля. Масив на верхньому рівні теж валідний, але він менш зручний, якщо ви захочете поруч додати ще щось, окрім елементів списку.
Якщо вам хочеться відчути це правило на Java (без парсингу, просто як звичку дивитися на форму), можна зробити зовсім маленький приклад. Він не розбирає JSON — він просто показує, що верхній рівень читається навіть за першим символом:
// Приклад JSON-рядка (саме рядка, без парсингу).
String json = """
{ "items": [ { "id": 1, "title": "Clean Code" } ], "count": 1 }
""";
// Прибираємо пробіли та перенесення рядків по краях, щоб перший символ точно був { або [.
char topLevel = json.strip().charAt(0);
System.out.println(topLevel); // Очікуємо '{' — отже, на верхньому рівні об’єкт.
Тут strip() прибирає пробіли та перенесення рядків по краях, а charAt(0) бере перший «справжній» символ. Це дуже проста річ, але вона тренує правильний рефлекс: спочатку форма, потім деталі.
3. Дані та контекст
Коли верхній рівень — об’єкт, мозок новачка часто намагається читати його зліва направо, як текст. Але JSON — не роман, а структура. Тому корисно одразу розділити поля на дві групи: ті, де лежить основне «корисне навантаження» (зазвичай масив або великий об’єкт), і ті, що описують це навантаження: count, page, requestId.
Уявіть, що ви відкрили JSON і бачите п’ять полів. Питання не «яке поле перше», а «яке поле головне». Зазвичай головне поле виглядає як великий контейнер, тобто об’єкт {...} або масив [...]. А поруч із ним лежать прості значення: числа, рядки, булеві значення.
Щоб не потонути в структурі, у парі прикладів я підпишу частини відповіді коментарями. Це вже jsonc, а не валідний JSON: у справжньому JSON коментарів немає, але для читання структури так зрозуміліше.
Наприклад, ось типова форма відповіді пошуку (спрощена):
{
// Основні дані: список результатів
"results": [
{ "title": "Clean Code", "year": 2008 },
{ "title": "Refactoring", "year": 1999 }
],
// Метадані: скільки результатів повернулося
"count": 2
}
Тут results — це основний масив даних, а count — метадані про нього. І вам психологічно легше читати так: «у відповіді є results; results — це список; у списку 2 елементи; окремо написано count=2». Якщо ви читаєте в іншій послідовності, ви спочатку зачепитеся за count, потім стрибнете в масив, а потім загубите, де ви були.
Є ще один типовий джерело плутанини, яке важливо проговорити прямо зараз, поки ми не пішли глибше: поле status у тілі JSON не дорівнює HTTP-статусу відповіді. Це різні рівні світу.
Порівняйте:
| Де ми перебуваємо | Що таке status | Приклад |
|---|---|---|
| HTTP-рівень | Код відповіді сервера | 200 OK, |
| JSON-рівень (у body) | Просто поле в даних | або |
Наприклад, відповідь /health у нашому майбутньому локальному API може виглядати так:
{
// Це поле в тілі JSON, а не HTTP-статус відповіді
"status": "UP",
"appName": "readlater-api"
}
І при цьому HTTP-статус буде 200 OK. Поле "status": "UP" — це частина контракту тіла відповіді, а не транспортного рівня HTTP. Це схоже на ситуацію: у листі написано «все добре», але конверт усе одно може бути порваний. Конверт — це HTTP, лист — це JSON.
4. Вкладеність як шлях
Вкладеність лякає, коли ви намагаєтеся тримати весь документ у голові одночасно. Хитрість тут — мислити не «весь JSON одразу», а конкретним маршрутом до потрібного значення. Такий маршрут можна записувати словами («results → перший елемент → authors → перший автор») або в більш програмній формі на кшталт results[0].authors[0].
Сама ідея дуже проста: якщо JSON — це структура, то до будь-якого значення можна дістатися «по драбині». Ми йдемо згори вниз, обираючи або поле об’єкта, або елемент масиву.
Давайте візьмемо невеликий вкладений приклад, де є і масив, і масив усередині об’єкта:
{
// Головний контейнер даних: масив результатів
"results": [
{
"title": "Clean Code",
"authors": ["Robert C. Martin"],
"year": 2008
}
],
// Метадані: загальна кількість елементів
"count": 1
}
Тепер запишемо кілька «шляхів» і що вони означають. Я використовуватиму зрозумілий програмістський запис: . для поля об’єкта і [0] для елемента масиву.
| Шлях | Як читати по-людськи | Що отримуємо |
|---|---|---|
| count | поле count на верхньому рівні | 1 |
| results | поле results на верхньому рівні | масив із 1 елемента |
| results[0] | перший елемент масиву results | об’єкт книги |
| results[0].title | поле title усередині першої книги | "Clean Code" |
| results[0].authors[0] | перший автор першої книги | "Robert C. Martin" |
Важливо: індекс [0] — це «перший елемент», тому що в Java, як ви пам’ятаєте, індексація з нуля. Це класичне джерело помилок: «чому другий елемент став першим». Під час читання JSON ви не зобов’язані записувати шлях саме так, але для майбутнього коду корисно мислити одразу в Java-координатах.
Ще один важливий момент: цей «шлях» — не бібліотека і не стандарт, ми не підключаємо жодних JSONPath-рішень і не робимо магії. Це просто зручна мова, щоб ваш мозок (і ваші нотатки) не перетворювалися на кашу. Коли ви пізніше читатимете реальні відповіді в Postman, такий запис дуже допомагає: ви можете прямо собі написати «мені потрібне значення з results[0].title» — і одразу зрозуміло, де його шукати.
5. Масив об’єктів: елемент як шаблон
Найпоширеніша конструкція в API — масив об’єктів: список книг, список користувачів, список елементів списку читання. Помилка новачка — намагатися очима пробігтися одразу по всьому масиву й потонути в повторюваних шматках. Набагато спокійніше спочатку витягнути один елемент і зрозуміти його форму: які поля є і які з них вкладені.
Чому це працює? Бо масив об’єктів майже завжди означає «багато однотипних сутностей». Тобто кожен елемент масиву — це «книга», і в кожної книги приблизно однаковий набір полів (хоча деякі можуть бути відсутні або null — але сенс цього ми розберемо в наступній лекції, без спойлерів).
Подивіться на масив і подумки скажіть собі: «Окей, це список. Я беру перший елемент як приклад. Розумію, що всередині. Потім уже думаю про другий, десятий і так далі».
Ось трохи складніший приклад, із вкладеним об’єктом і вкладеним масивом:
{
"results": [
{
"title": "Clean Code",
"meta": { "language": "en", "pages": 464 },
"authors": ["Robert C. Martin"]
}
],
"count": 1
}
Якщо ви спочатку зрозумієте форму одного елемента results[0], далі все стає передбачуваним. Ви вже знаєте, що десь там є title, meta.language, meta.pages, authors[0]. А коли побачите другий елемент масиву, ви перевірятимете не «все підряд», а «а він такої ж форми?».
До речі, це дуже близько до того, як ми потім писатимемо код у клієнтській частині проєкту: спочатку розуміємо структуру даних, потім будуємо обробку. Але сьогодні ми лишаємося на рівні читання й розуміння, без серіалізації та без Java-об’єктів.
6. Приклад ReadLater: відповідь списку читання
Давайте прив’яжемо техніку до нашого наскрізного проєкту ReadLater Starter. Згодом у нас з’явиться локальний API, який віддаватиме список книг для читання. Відповідь буде не просто «голим масивом», а об’єктом-обгорткою з полем items (сам список) і count (скільки всього елементів). Це чудовий навчальний приклад вкладеності без зайвої екзотики.
Уявімо майбутню відповідь ендпоінта GET /api/v1/reading-list (ми поки не реалізуємо сервер, ми просто вчимося читати форму даних):
{
// Основні дані: список елементів списку читання
"items": [
{
"id": 1,
"title": "Clean Code",
"author": "Robert C. Martin",
"status": "PLANNED",
"externalId": "OL12345M"
}
],
// Метадані: скільки всього елементів у items
"count": 1
}
Тепер читаємо його спокійно, по кроках, як за інструкцією до шафи з IKEA (лише без зайвих гвинтів, сподіваюся):
Спочатку верхній рівень: це об’єкт {...}. Отже, на верхньому рівні ми шукаємо поля. Бачимо два поля: items і count. Уже на цьому етапі можна сказати: «ага, це обгортка; items — імовірно основні дані; count — метадані».
Далі поле items: воно починається з [ — отже, це масив. Відповідно, усередині items лежить список елементів. Ми дивимося на перший елемент items[0] і бачимо {...} — об’єкт. Отже, кожен елемент списку — це об’єкт одного елемента reading list. І в нього є поля id, title, author, status, externalId.
І ось вам готові «шляхи» до значень:
| Що хочемо дізнатися | Шлях | Приклад значення |
|---|---|---|
| Скільки всього елементів у відповіді | count | 1 |
| Назва першої книги | items[0].title | |
| Статус першої книги | items[0].status | |
| Зовнішній ідентифікатор | items[0].externalId | |
Якщо ви хочете пов’язати це з кодом (знову ж таки: без парсингу, просто як звичку тримати «приклад відповіді» поруч), зручно зберігати такі JSON-фрагменти в Java як text block. Це нормально на навчальному етапі, коли ви вчитеся читати структуру очима:
// У навчальних прикладах зручно тримати «приклад відповіді» просто рядком.
String readingListJson = """
{
"items": [
{ "id": 1, "title": "Clean Code", "status": "PLANNED" }
],
"count": 1
}
""";
// Проста перевірка: чи є в тексті потрібне поле? Це НЕ парсинг JSON.
System.out.println(readingListJson.contains("\"items\"")); // true
Останній рядок не розбирає JSON, а просто показує: це звичайний текст, і ви можете виконувати з ним найпростіші дії. Пізніше у нас з’явиться нормальна обробка JSON у коді, але сьогодні важливе саме розуміння структури.
7. JSON як дерево
Людські очі люблять структуру, а не нескінченні лапки й коми. Тому, коли JSON стає вкладеним, корисно на хвилину забути про «текст» і намалювати дерево: корінь документа, його дочірні поля, де масиви, де об’єкти і що повторюється. Це схоже на карту метро: по ній простіше зрозуміти, де ви перебуваєте.
Візьмімо той самий приклад списку читання й зобразімо його як дерево. Це не «формальний стандарт», а просто дуже корисна шпаргалка:
// Дерево JSON: у дужках указано тип вузла (object/array/string/number).
root (object)
├─ items (array)
│ └─ [0] (object)
│ ├─ id (number)
│ ├─ title (string)
│ ├─ author (string)
│ ├─ status (string)
│ └─ externalId (string)
└─ count (number)
Коли ви бачите таку картинку, одразу стає зрозуміло кілька речей. По-перше, items — повторюваний блок, і його структура — структура одного елемента. По-друге, count не «десь там поруч», а на верхньому рівні. По-третє, якщо ви описуватимете контракт словами, ви можете сказати: «відповідь — об’єкт із полями items і count; items — масив об’єктів; у об’єкта є id/title/author/status/externalId».
Якщо хочеться ще наочніше, можна уявити те саме як потік: корінь → контейнер → елементи. Mermaid-схема тут працює як зручний спосіб не переплутати рівні:
flowchart TD
%% Візуалізація рівнів: root -> items -> елемент масиву; count лежить на верхньому рівні.
A["корінь: об’єкт"] --> B["items: масив"]
B --> C["[0]: об’єкт"]
C --> D["title: рядок"]
C --> E["status: рядок"]
A --> F["count: число"]
Не бійтеся малювати такі схеми навіть на папері. Це не ознака слабкості, це ознака того, що ви не намагаєтеся бути «компілятором на ніжках». Нехай компілятор лишається компілятором, а ми будемо інженерами: спочатку розуміємо структуру, потім пишемо код.
8. Типові помилки під час читання вкладеного JSON
Помилка №1: починати читання «з середини», тому що око зачепилося за знайоме слово.
Дуже хочеться побачити десь усередині "title" і відразу зробити висновок: «Ага, значить це книга». Але ви втрачаєте контекст: де саме цей title живе — на верхньому рівні, усередині book, усередині results[0]? Правильна звичка — спочатку подивитися на верхній рівень {} або [], потім визначити головний контейнер (items/results), і лише після цього йти до конкретних полів по шляху.
Помилка №2: плутати об’єкт і масив, бо обидва схожі на якусь конструкцію в дужках.
Це смішно рівно до першої реальної відповіді, де у вас items: [], і ви раптом намагаєтеся шукати поле items.title. В об’єкта є поля за іменами, у масиву є елементи за індексом. Якщо в голові не перемкнути режим, ви будете «діставати поле зі списку», а список дивитиметься на вас мовчки й осудливо.
Помилка №3: читати об’єкт зліва направо й надавати значення порядку полів.
У JSON-об’єкті порядок полів не має смислового значення. Сьогодні API прислав count після items, завтра — перед items, післязавтра додав requestId у середину. Якщо ви прив’язуєтеся очима до порядку, ви відчуватимете, що контракт «постійно змінюється», хоча змінюється лише форматування. Тримайтеся за імена полів і структуру контейнерів, а не за позицію в тексті.
Помилка №4: змішувати поля верхнього рівня та поля елементів масиву.
Класичний випадок: є count поруч із items, а в кожному item ще є id. Новачок починає сприймати count як «ще одне поле елемента», хоча воно належить усій відповіді. Тут допомагає дерево: count живе на кореневому рівні, а id — на рівні items[0]. Важливо проговорювати це вголос хоча б перші кілька разів.
Помилка №5: плутати status у тілі JSON зі статусом HTTP-відповіді.
Якщо ви бачите { "status": "UP" }, це не означає, що сервер повернув HTTP 200 (хоча зазвичай саме так і є). І навпаки: сервер може повернути HTTP 500, а в тілі відповіді буде зовсім інша структура (або тіла взагалі не буде). Тримайте в голові два шари: транспортний (HTTP status/headers) і прикладний (JSON-поля).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ