JavaRush /Курси /Swift SELF /JSON — об’єкт/масив/примітиви

JSON — об’єкт/масив/примітиви

Swift SELF
Рівень 58 , Лекція 0
Відкрита

1. Навіщо нам JSON

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

Проблема починається там, де новачок думає: «Ну це ж просто текст, зараз розберемо як-небудь». А потім раптом з’ясовується, що один і той самий ключ може бути числом, рядком або взагалі бути відсутнім — і ваша програма перетворюється на детектив: «а де ж поле id, і чому воно сьогодні "7", а не 7?». Саме тому ми говоритимемо не лише про «синтаксис JSON», а й про схему: домовленість про те, що саме лежить усередині.

Щоб пов’язати це з нашим навчальним застосунком, уявімо, що ми створюємо консольну мінібібліотеку — умовний LibraryCLI. Ми додаємо книги, шукаємо їх, а одного дня захочемо ще й зберегти дані. Для цього нам потрібен формат — і JSON якраз один із найтиповіших варіантів.

2. Структура JSON: контейнери і примітиви

JSON тримається на дуже простій ідеї: є контейнери (складені типи), які можуть містити інші значення, і є примітиви (атомарні значення). Це як коробки та предмети. Коробка може містити інші коробки й предмети, а предмет усередині себе вже нічого не зберігає.

У JSON лише два контейнери: object і array. І лише чотири «прості» типи значень: string, number, bool, null. Якщо ви це запам’ятаєте — ви вже на півдорозі до спокійного життя.

Для наочності — маленька таблиця:

Категорія Вигляд у JSON Приклад
Object (об’єкт)
{ ... }
{ "title": "Dune" }
Array (масив)
[ ... ]
[1, 2, 3]
String (рядок)
"..."
"Swift"
Number (число)
123, 3.14
2024
Bool (логічне)
true / false
false
Null
null
null

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

3. JSON‑об’єкт {}: ключ → значення

JSON‑об’єкт — це набір пар «ключ → значення». У побуті зручно думати про нього як про Dictionary, тільки з жорсткими правилами: ключі завжди рядки і завжди пишуться в подвійних лапках. Тобто {"id": 1} — так, {id: 1} — ні (це вже «майже JSON», який любить JavaScript, але JSON такий трюк не схвалює).

З практичного погляду об’єкт добрий тоді, коли в сутності є іменовані поля: id, title, author. Саме в такому вигляді ми б описали книгу в нашому застосунку.

Приклад — просто рядок, без парсингу:

let bookJSON = """
{
  "id": 1,
  "title": "Dune",
  "isAvailable": true
}
"""

print(bookJSON.count) // наприклад, 55 (залежно від пробілів і перенесень рядків)

Зверніть увагу на деталі: ключі в лапках, значення різних типів, і JSON не заперечує, що id — число, title — рядок, isAvailable — логічне. Це нормально і зручно.

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

4. JSON‑масив []: список значень

Масив у JSON — це послідовність значень, розділених комами. Значення можуть бути будь-якими: примітивами, об’єктами, масивами. Якщо об’єкт — це «картка з полями», то масив — це «список карток».

Для нашої мінібібліотеки масив — природна форма для списку книг: [book1, book2, book3].

let tagsJSON = #"""
["swift", "json", "cli"]
"""#

print(tagsJSON) // ["swift", "json", "cli"]

Чому тут #""" ... """#? Це raw-рядок: усередині можна писати подвійні лапки без екранування. Ми не зобов’язані використовувати його, але іноді так простіше читати очима.

Ще один приклад — масив об’єктів:

let booksJSON = """
[
  { "id": 1, "title": "Dune" },
  { "id": 2, "title": "1984" }
]
"""

print(booksJSON.contains("\"title\"")) // true

Головна практична думка: якщо корінь JSON — масив, то на верхньому рівні у вас немає місця для метаданих. Це стане важливо, коли ми захочемо зберігати версію схеми, дату оновлення, джерело даних тощо — поки просто запам’ятаємо цю думку.

5. Примітиви JSON: string / number / bool / null

Примітиви здаються простими, поки ви не починаєте їх зберігати «по-справжньому». Саме на примітивах найчастіше й ламаються очікування: "7" (рядок) раптово з’являється замість 7 (числа), null з’являється там, де ви чекали рядок, а true перетворюється на "true" (рядок) — і все, маємо проблему.

У JSON примітиви такі:

  • string: лише в подвійних лапках, з екрануванням спеціальних символів, зокрема перенесення рядка й лапок.
  • number: у JSON немає окремих Int і Double, це просто «число». Усередині реалізації воно може стати цілим або дробовим, але в тексті це одна категорія.
  • bool: лише true або false малими літерами.
  • null: спеціальне значення, яке означає відсутність даних.
let primitivesJSON = """
{
  "title": "Dune",
  "year": 1965,
  "rating": 4.8,
  "isClassic": true,
  "subtitle": null
}
"""

print(primitivesJSON.contains("null")) // true

З погляду схеми — тобто домовленості — важливо вирішити: що означає null? Це «значення невідоме», «значення навмисно порожнє», «значення видалено»? JSON дозволяє написати null, але зміст задаєте ви.

6. Вкладеність: JSON як дерево даних

Майже будь-який реальний JSON — вкладений. Усередині об’єкта лежить масив, усередині масиву лежать об’єкти, усередині них знову масиви… і так далі. Це виглядає як дерево, де кожна гілка — це або масив, або об’єкт, а листки — примітиви.

Для книги це може бути так: об’єкт книги містить масив авторів (рядків), масив тегів, а ще вкладений об’єкт meta (наприклад, «додано до вибраного»).

let nestedJSON = """
{
  "id": 1,
  "title": "Dune",
  "authors": ["Frank Herbert"],
  "meta": { "addedBy": "user", "isFavorite": false }
}
"""

print(nestedJSON.contains("\"meta\"")) // true

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

Можна навіть намалювати «форму» даних:

flowchart TD
    A[JSON-об’єкт книги] --> B["id: число"]
    A --> C["title: рядок"]
    A --> D["authors: масив"]
    D --> E["author: рядок"]
    A --> F["meta: об’єкт"]
    F --> G["addedBy: рядок"]
    F --> H["isFavorite: логічне"]

7. Схема і дизайн зберігання для LibraryCLI

Що таке схема і чому без неї складно

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

Схема (у нашому навчальному сенсі) — це домовленість про чотири речі:

1) який тип у кореня: об’єкт {} або масив [],
2) які ключі мають об’єкти і як вони називаються,
3) які типи мають значення (рядок/число/масив/об’єкт…),
4) які поля обов’язкові, а які можуть бути відсутніми.

Чому це важливо? Бо Swift суворий: якщо ви написали, що в книги є id: Int, то ви внутрішньо пообіцяли собі, що з даних завжди можна отримати ціле число. А JSON вам нічого такого не обіцяв. Саме це розходження — ключове джерело помилок і головного болю під час роботи з реальними даними.

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

«Немає значення»: ключ відсутній чи ключ є, але null

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

1) ключ відсутній взагалі
2) ключ є, але значення null

Порівняйте:

let missingKeyJSON = """
{ "id": 1, "title": "Dune" }
"""

let nullKeyJSON = """
{ "id": 1, "title": "Dune", "subtitle": null }
"""

print(missingKeyJSON.contains("subtitle")) // false
print(nullKeyJSON.contains("subtitle"))    // true

І ось питання схеми: що для вашого застосунку означає відсутність subtitle? А що означає subtitle: null? Це один і той самий стан чи різні?

У форматі зберігання нашої бібліотеки зазвичай зручно вважати, що subtitle може бути необов’язковим: іноді його немає — і це нормально. Але навіть у цьому разі краще обрати один стиль: або поле може бути відсутнім, або воно завжди є, але іноді null. Обидва підходи життєздатні — важливо, щоб ви не змішували їх випадково.

Як виглядатиме файл цілком: корінь і метадані

Зараз буде дуже практичний момент: ми ще не пишемо Codable-моделі й не кодуємо/декодуємо, але вже вирішуємо, як виглядатиме файл. Це рішення — як вибір планування квартири: потім можна переставити меблі, але знести несучу стіну складніше.

Уявімо, що ми хочемо зберігати список книг. У книги нехай будуть такі поля, як ідея: id, title, необов’язково subtitle, масив authors, масив tags.

Як може виглядати один Book у JSON? Приклад:

let bookV1JSON = """
{
  "id": 1,
  "title": "Dune",
  "subtitle": null,
  "authors": ["Frank Herbert"],
  "tags": ["sci-fi", "classic"]
}
"""

print(bookV1JSON.contains("\"authors\"")) // true

Тепер ключовий вибір схеми: що буде коренем файла?

Якщо корінь — масив книг, то це просто і компактно:

[
  { ...book1... },
  { ...book2... }
]

Але щойно ви захочете зберігати метадані, наприклад версію схеми, масив вас обмежуватиме: метадані нікуди покласти. Тому в інженерній практиці часто роблять коренем об’єкт-контейнер, де є метаполя і масив даних.

Дуже типовим полем є "schemaVersion": воно трапляється в реальних JSON-форматах як механізм «версіювання схеми». Наприклад, у форматах навколо SwiftPM теж використовують поле "schemaVersion" у JSON-маніфестах, щоб розрізняти версії структури файла.

Ми тут поки не фіксуємо остаточний формат — це буде окрема лекція дня, — але важлива думка вже зараз: схема — це не лише “як виглядає Book”, а й “як виглядає файл цілком”.

8. Швидкі перевірки та валідність JSON

Мініперевірка форми JSON «на око»

Іноді корисно вміти швидко подивитися на JSON як на текст і зрозуміти, чи корінь — об’єкт, чи масив, не виконуючи справжнього парсингу. Це не заміна нормальному декодуванню, але як діагностика — працює.

func jsonRootKind(_ text: String) -> String {
    let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
    if trimmed.hasPrefix("{") { return "object" }
    if trimmed.hasPrefix("[") { return "array" }
    return "unknown"
}

print(jsonRootKind("{\"id\":1}")) // object
print(jsonRootKind("[1,2,3]"))    // array

Це навмисно примітивна перевірка, але вона допомагає новачку перестати плутатися: {} і [] — це два різні «контейнери», і від цього залежить, як ви інтерпретуватимете дані.

Валідний JSON проти «майже JSON»

JSON суворий і часто не прощає дрібних відхилень. Особливо боляче тим, хто звик до мов і форматів, де дозволені коментарі, одинарні лапки й «кома на удачу».

Ось типовий «майже JSON», який виглядає правдоподібно, але є невалідним JSON:

Приклад — як рядок компілюється, але це не валідний JSON:

let almostJSON = """
{
  'id': 1, // одинарні лапки і коментарі
  "title": "Dune",
}
"""

print(almostJSON) // виглядає схоже, але це не JSON

Тут одразу три проблеми: одинарні лапки, коментарі, кома після останнього елемента. І це не придирки: якщо ви зберігаєте дані у файлі, будь-яка така «дрібниця» перетворює читання на помилку.

9. Типові помилки

Помилка №1: плутати JSON і «об’єкт у JavaScript».
Дуже часта історія: людина пише { id: 1 } і щиро вважає, що це JSON. Це схоже на JSON, але JSON вимагає, щоб ключі були рядками в подвійних лапках: { "id": 1 }. Якщо тримати в голові правило «у JSON ключ — завжди рядок», ви перестаєте наступати на ці граблі.

Помилка №2: не домовитися про корінь і постійно змінювати підхід.
Сьогодні ви зберегли файл як масив книг, завтра вирішили, що потрібен об’єкт із метаданими, а післязавтра додали ще один масив поруч. У результаті старі дані читати складно, нові — ще складніше. Корінь файла — частина схеми: обрали — тримайтеся.

Помилка №3: змішувати типи «за настроєм»: id то число, то рядок.
JSON дозволяє зберігати "id": "7" і "id": 7, але для застосунку це два різні варіанти. Якщо ви хочете числовий ідентифікатор — зберігайте його числом. Якщо хочете рядковий — зберігайте рядком. Змішування майже завжди призводить до того, що половину часу ви «лагодите дані», а не розвиваєте програму.

Помилка №4: плутати відсутність ключа і null.
В одному місці ви не записали subtitle, в іншому записали subtitle: null, у третьому записали subtitle: "". А потім намагаєтеся зрозуміти, чому в інтерфейсі десь показується «(немає підзаголовка)», а десь — порожній рядок, а десь узагалі падає логіка. Ці випадки можуть бути різними станами — але тоді це має бути частиною схеми. Якщо вам не потрібна така тонкість, оберіть один варіант і дотримуйтеся його.

Помилка №5: змінювати імена ключів «заради краси».
Сьогодні у вас "schemaVersion", завтра ви вирішили, що "schema" коротше, а післязавтра — що краще "version". Такі зміни ламають сумісність і перетворюють просте читання JSON на міграції та костилі. У реальних інструментах, які зберігають JSON-файли, ключі і "schemaVersion" зазвичай фіксують як частину контракту формату.

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