JavaRush /Курси /Java Server /JSON-обʼєкт і JSON-масив

JSON-обʼєкт і JSON-масив

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

1. Два «контейнери» JSON: обʼєкт і масив

Коли ви вперше бачите JSON у відповіді API, хочеться читати його як книжку: зліва направо, зверху вниз, «ось тут про статус, ось тут про назву…». Але JSON — не роман, а структура. Майже завжди вона починається з одного з двох контейнерів: обʼєкта { ... } або масиву [ ... ].

Ці два контейнери розв’язують дуже прикладну задачу. Обʼєкт зручний, коли ми описуємо одну сутність — одну книжку, один елемент списку читання, одну помилку. Масив зручний, коли ми описуємо набір сутностей — список книжок, результати пошуку, список елементів reading list. Можна думати так: обʼєкт — це «папка з підписаними файлами», а масив — це «черга елементів».

Щоб швидше орієнтуватися, корисно мати в голові маленьку «шпаргалку» — не у вигляді зубріння, а як візуальну звичку:

Що бачу на початку JSON Що це означає Типовий зміст в API
{ JSON-обʼєкт «Одна сутність» або «відповідь-обгортка з кількома полями»
[ JSON-масив «Список однотипних елементів»

Також буває, що API повертає на верхньому рівні масив, але значно частіше ви побачите обʼєкт, усередині якого захований масив (наприклад, items) і поруч лежить щось на кшталт count. Чому так роблять — обговоримо ближче до середини лекції.

2. JSON-обʼєкт: набір полів імʼя: значення

JSON-обʼєкт — це основний спосіб описувати «одну річ» у форматі, який зрозумілий і людині, і машині. Він схожий на звичну структуру: у кожного елемента є назва поля (ключ) і значення. Головне — памʼятати, що JSON набагато суворіший, ніж здається: тут не можна «приблизно якось», тут або валідно, або ні.

Коли бекенд віддає обʼєкт, він ніби каже клієнту: «Ось набір іменованих властивостей. Бери те, що тобі потрібно». І це дуже відрізняється від масиву, де зміст елементів часто тримається на домовленості про порядок. В обʼєкті порядок не має бути джерелом змісту: зміст тримається на назвах полів.

Синтаксис обʼєкта

Зараз буде трохи граматики, але без неї JSON перетворюється на «будь-який текст у фігурних дужках», який ламається від однієї зайвої коми. JSON-обʼєкт починається з { і закінчується }. Усередині — поля, розділені комами. Кожне поле записується як "імʼя": значення, де назва поля — рядок у подвійних лапках.

Ось мінімальний приклад обʼєкта у стилі нашого проєкту ReadLater Starter (один елемент списку читання):

// Приклад: один елемент списку читання у вигляді JSON-обʼєкта (одна сутність)
String itemJson = """
{
  "id": 1,
  "title": "Clean Code",
  "author": "Robert C. Martin",
  "status": "PLANNED"
}
""";

// Тут ми не розбираємо JSON, а просто показуємо форму і синтаксис
System.out.println(itemJson); // друкує JSON-обʼєкт (одна сутність)

Для обʼєкта тут достатньо трьох опорних правил. Назва поля в JSON — це завжди рядок у подвійних лапках. Між назвою і значенням стоїть двокрапка, а поля одне від одного відділяються комами. Кома після останнього поля не ставиться — JSON не любить «висячі» коми, навіть якщо вам здається, що вони милі й самотні.

Поле обʼєкта: назва і тип значення

Поле обʼєкта — це пара «назва → значення». І ось тут важливо памʼятати попередню тему про типи значень: значення може бути рядком, числом, булевим, null, обʼєктом або масивом. Інших варіантів у JSON немає — формат навмисно обмежений, щоб бути передбачуваним.

Подивімося на приклад, де в одному обʼєкті зібрано різні типи значень. Це схоже на реальний вміст відповіді бекенду: частина полів обовʼязкова, частина — службова, а частина може бути відсутньою або бути null (про це докладніше в наступній лекції).

// Приклад: один обʼєкт, але значення різних JSON-типів (рядок, число, булевий, null)
String responseJson = """
{
  "status": "UP",
  "uptimeSeconds": 125,
  "healthy": true,
  "comment": null
}
""";

// Важливо очима бачити різницю: "125" (рядок) і 125 (число) — це не одне й те саме
System.out.println(responseJson); // друкує JSON-обʼєкт із різними типами значень

Тут легко зловити себе на тому, що ви читаєте значення очима, але губите типи. Наприклад, "125" і 125 для людини виглядають дуже схоже, але для контракту це два різні світи. У першому випадку це текст, який не можна чесно складати як числа; у другому — число, яке можна порівнювати, підсумовувати тощо. Тому хороший бекенд-розробник під час читання JSON завжди тримає в голові: «у лапках — рядок, без лапок — не рядок».

Обʼєкт як сутність: приклади ReadLater

Зараз зробимо важливий практичний крок: привʼяжемо «обʼєкт» до домену, а не до теорії. У ReadLater Starter у нас будуть відповіді, які логічно є однією річчю: один елемент списку читання, одна помилка, одна health-відповідь. І майже завжди такі відповіді — саме JSON-обʼєкти.

Наприклад, один елемент reading list може виглядати так (це вже дуже близько до майбутнього ReadingItemResponse):

// Приклад: JSON-обʼєкт елемента читання (одна сутність) — поля доменної моделі, а не «просто текст»
String readingItemResponse = """
{
  "id": 42,
  "title": "Refactoring",
  "author": "Martin Fowler",
  "status": "IN_PROGRESS",
  "externalId": "OL12345M",
  "comment": "Читати по розділу на день"
}
""";

// Клієнт зазвичай читає поля за назвою: readingItemResponse.id, readingItemResponse.status тощо.
System.out.println(readingItemResponse); // один обʼєкт = один елемент списку

А ось приклад обʼєкта помилки (у нас у проєкті буде єдиний формат помилок, але поки ми просто дивимося на форму). Зверніть увагу: це не «текст помилки», це структура.

// Приклад: помилку теж віддають як обʼєкт (контракт!), а не як «рядок з образами»
String errorResponse = """
{
  "errorCode": "READING_ITEM_NOT_FOUND",
  "message": "Елемент списку читання не знайдено",
  "details": "id=999"
}
""";

Зміст тут такий: бекенд не просто «лається», а повідомляє клієнту дані про помилку у передбачуваному вигляді. Навіть якщо ви поки не пишете код, який це обробляє, важливо звикнути бачити в цьому контракт, а не декоративний JSON.

3. JSON-масив: список обʼєктів

JSON-масив — це другий базовий контейнер, який відповідає за «багато елементів». Якщо обʼєкт — це «одна сутність з іменованими полями», то масив — це «послідовність елементів». У масиві немає назв для кожного елемента: елементи розрізняються насамперед позицією — перший, другий, третій.

Масиви трапляються в API постійно: результати пошуку, списки сутностей, списки авторів, списки тегів. І тут важливо не переплутати два схожі, але різні сценарії: «масив простих значень» (наприклад, список рядків) і «масив обʼєктів» (наприклад, список книжок, кожна з яких — обʼєкт).

Синтаксис масиву

Масив починається з [ і закінчується ]. Усередині — елементи, розділені комами. Кожен елемент масиву — це JSON-значення, тобто він може бути рядком, числом, булевим, null, обʼєктом або навіть іншим масивом.

Ось простий приклад масиву рядків (так теж буває в API, наприклад список авторів або список тегів):

// Приклад: масив простих значень (рядків)
String authorsJson = """
[
  "Robert C. Martin",
  "Martin Fowler",
  "Kent Beck"
]
""";

// Порожній масив [] теж нормальний: це «список є, просто він порожній»
System.out.println(authorsJson); // JSON-масив рядків

І знову та сама сувора дисципліна: рядки в подвійних лапках, кома між елементами, жодної коми в кінці. У масивів є хороша властивість: порожній масив [] — це абсолютно нормальне значення, і воно часто означає «список відомий, але він порожній».

Порядок і однотипність елементів

У масиву є смислова особливість, яку легко пропустити: порядок елементів важливий. Якщо в обʼєкті порядок полів не має бути носієм змісту, то в масиві порядок — це частина даних. Якщо API віддає список книжок, то «перша» книжка і «друга» книжка — різні елементи, навіть якщо вони однакові за структурою.

Водночас у прикладних API майже завжди очікується, що елементи масиву будуть одного типу і однієї форми. Тобто якщо results — це масив обʼєктів книжок, то кожен елемент results зазвичай має однаковий набір полів: title, author, year тощо. Строго кажучи, JSON дозволяє мішанину типів — перший елемент рядок, другий обʼєкт, третій число, — але в API це майже завжди виглядає як помилка проєктування контракту.

Невеликий приклад масиву обʼєктів, який близький до нашої теми «список книжок»:

// Приклад: масив обʼєктів (кожен елемент масиву — окрема сутність)
String booksJson = """
[
  { "id": 1, "title": "Clean Code" },
  { "id": 2, "title": "Refactoring" }
]
""";

// Важливо: клієнт не «дістає title з масиву», він перебирає елементи й читає поля в кожного
System.out.println(booksJson); // JSON-масив обʼєктів (список сутностей)

Тут важливо втримати думку: масив — це контейнер «багато». А те, що всередині, часто є обʼєктами, тобто кожен елемент списку — окрема сутність.

Масив в API: reading list items

Тепер зробимо крок від абстракції до типового API-патерну. У реальному API список елементів часто виглядає як масив обʼєктів. Наприклад, уявіть, що в нашому ReadLater API ми хочемо повернути кілька елементів списку читання. Логічно очікувати масив items.

// Приклад: список елементів читання як масив обʼєктів
String itemsArrayJson = """
[
  { "id": 1, "title": "Clean Code", "status": "PLANNED" },
  { "id": 2, "title": "Refactoring", "status": "IN_PROGRESS" }
]
""";

Поки ми читаємо це очима, усе просто. Але важливо звикнути до того, як це виглядатиме для клієнта: він проходитиме по масиву, братиме кожен обʼєкт і читатиме в нього поля. Саме тому однакова форма елементів масиву така важлива: якщо один обʼєкт раптом буде без поля status, клієнту доведеться ускладнювати обробку. Тема «null vs missing» — це якраз наступна лекція.

4. Верхній рівень відповіді API: обʼєкт-обгортка

У новачків іноді виникає чесне запитання: «Якщо я віддаю список, чому б не повернути просто [...] і не жити спокійно?» У теорії можна. На практиці бекенд часто повертає обʼєкт-обгортку, усередині якого лежить масив, а поруч — додаткові поля на кшталт count. Це не «шкідливість» і не «любов до зайвих дужок», а інженерна звичка робити контракт розширюваним.

Суть проста: якщо ви повертаєте лише масив, то потім вам складніше додати до відповіді додаткові поля — наприклад, загальну кількість, підказки, метаінформацію — не ламаючи клієнтський код. А якщо верхній рівень — обʼєкт, то додати поле поряд із масивом набагато простіше: форма відповіді залишається обʼєктом, і клієнт не змушений перебудовувати базову логіку.

Класичний приклад у нашому домені — відповідь зі списком reading list items:

// Приклад: обʼєкт-обгортка, всередині якого масив items, а поруч метадані (count)
String readingListResponse = """
{
  "items": [
    { "id": 1, "title": "Clean Code", "status": "PLANNED" }
  ],
  "count": 1
}
""";

// Цей патерн допомагає розширювати відповідь новими полями, не ламаючи клієнтський код
System.out.println(readingListResponse); // обʼєкт-обгортка + масив items + count

Зверніть увагу на тонкий, але важливий момент: на верхньому рівні в нас обʼєкт { ... }, а не масив. При цьому масив усе одно є, просто він «схований» під ключем items. Це дуже типовий API-стиль, який ви будете бачити постійно — не лише в навчальних прикладах, а й у реальних сервісах.

Можна зафіксувати це у вигляді невеликої схеми — як «карта місцевості» для мозку:

flowchart TD
  A["JSON-тіло HTTP-відповіді"] --> B["{ ... } (обʼєкт)"]
  B --> C["items: [ ... ] (масив)"]
  B --> D["count: число"]
  C --> E["{ ... } (обʼєкт)"]
  C --> F["{ ... } (обʼєкт)"]

І ось це дерево вже читається не як «каша з дужок», а як дуже зрозуміла структура: відповідь → обʼєкт → усередині масив items → усередині кожен елемент — обʼєкт із полями.

5. Обʼєкт і масив як Java-моделі

Зараз ми свідомо не пишемо парсинг, не підключаємо бібліотеки і не перетворюємо JSON на Java-обʼєкти — це буде значно пізніше. Але вже сьогодні можна почати тренувати дуже корисну навичку: бачити в JSON форму майбутніх моделей даних. Це як дивитися на план квартири й уже розуміти, де буде кухня, навіть якщо ви поки тримаєте в руках лише олівець.

Для Java-розробника обʼєкт у JSON майже завжди означає «буде модель із полями», а масив означає «буде колекція елементів». Навіть якщо ви поки не знаєте, чи це буде record, звичайний клас або щось інше, думка залишається: обʼєкт схожий на структуру даних з іменованими властивостями, масив схожий на список однотипних елементів.

Наприклад, якщо ви бачите такий обʼєкт:

{ "id": 1, "title": "Clean Code" }

то мозок може просто зробити висновок: «Окей, це сутність, у неї є id і title». А якщо бачите таке:

{ "items": [ { "id": 1, "title": "Clean Code" } ], "count": 1 }

то ви вже можете прочитати: «Це відповідь списку. Усередині є масив елементів items, і поруч число count».

Щоб закріпити «погляд згори», іноді корисно зробити зовсім примітивну, майже шкільну перевірку: чим починається документ. Це не парсинг і не обробка JSON, а просто спосіб швидко зрозуміти форму верхнього рівня:

// Швидка перевірка форми верхнього рівня: обʼєкт чи масив
String json = readingListResponse.trim(); // прибираємо пробіли/переноси перед першим значущим символом
char first = json.charAt(0);              // беремо перший значущий символ

System.out.println(first);                      // {
System.out.println(first == '{' ? "ОБʼЄКТ" : "МАСИВ"); // ОБʼЄКТ

Це виглядає як дрібниця, але на практиці допомагає перестати «тонути» в середині документа. Спочатку форма: обʼєкт або масив. Потім уже вміст.

6. Типові помилки під час читання обʼєктів і масивів

Помилки з обʼєктами і масивами майже завжди трапляються не тому, що людина «погана», а тому, що вона намагається читати JSON очима як звичайний текст. JSON же вимагає поваги до структури: спочатку контейнер, потім поля чи елементи, потім типи значень. Якщо цю послідовність не тримати в голові, ви дуже швидко починаєте плутати масив з обʼєктом, а поле — з елементом масиву.

Помилка №1: переплутати {} і [], а потім намагатися «дістати поле» з масиву або «взяти елемент за індексом» з обʼєкта.
Це класика. В обʼєкті ви звертаєтеся до даних за назвою поля (title, status), а в масиві — перебираєте елементи за порядком. Тому насамперед варто звично дивитися на найзовнішній символ. Якщо він {, ви працюєте з набором полів. Якщо [, ви працюєте зі списком елементів.

Помилка №2: вважати, що обʼєкт — це «упорядкований список полів» і покладатися на порядок у контракті.
Так, багато інструментів друкують поля в одному й тому самому порядку, і здається, що так і має бути. Але в контрактному мисленні зміст в обʼєкті тримається на назві поля, а не на тому, що title іде раніше за author. Якщо ви починаєте «чекати», що потрібне поле завжди буде «вгорі», то будь-який інший сервіс або навіть інший реліз того самого сервісу може неприємно здивувати.

Помилка №3: робити масив «мішаниною» всього підряд: один елемент — рядок, другий — число, третій — обʼєкт.
JSON це дозволяє, але API-клієнтам так жити незручно. У реальних API масив майже завжди означає «список однотипних елементів». Якщо ви бачите мішанину типів, це зазвичай сигнал, що контракт погано продуманий або ви не так зрозуміли структуру (наприклад, сплутали поле верхнього рівня та елементи списку).

Помилка №4: забути про суворість лапок і поставити одинарні лапки в назвах полів.
Людське око спокійно сприймає { 'title': 'Clean Code' }, особливо якщо ви прийшли зі світу «схожих» синтаксисів. Але в обʼєкті назва поля — це теж рядок, тому тут працюють тільки подвійні лапки. Одна одинарна лапка ламає не «стиль», а сам документ.

Помилка №5: зайва кома в кінці обʼєкта або масиву.
Проблема прикра: структура вже зібрана, але після останнього поля або елемента залишилася кома «на автоматі». JSON цього не допускає, і документ розвалюється вже в самому кінці. Якщо бачите дивну помилку unexpected token — насамперед перевірте хвіст обʼєкта або масиву.

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