1. Навіщо нам JSON
Якщо ви колись зберігали нотатку в застосунку, надсилали дані на сервер або просто бачили відповідь від API, ви вже зустрічали JSON — навіть якщо не знали цього. JSON можна сприймати як дуже популярний формат «упакованого тексту», який однаково непогано читають і люди, і програми. Він не найшвидший і не найкомпактніший, але зазвичай зрозумілий і переносний.
Проблема починається там, де новачок думає: «Ну це ж просто текст, зараз розберемо як-небудь». А потім раптом з’ясовується, що один і той самий ключ може бути числом, рядком або взагалі бути відсутнім — і ваша програма перетворюється на детектив: «а де ж поле id, і чому воно сьогодні "7", а не 7?». Саме тому ми говоритимемо не лише про «синтаксис JSON», а й про схему: домовленість про те, що саме лежить усередині.
Щоб пов’язати це з нашим навчальним застосунком, уявімо, що ми створюємо консольну мінібібліотеку — умовний LibraryCLI. Ми додаємо книги, шукаємо їх, а одного дня захочемо ще й зберегти дані. Для цього нам потрібен формат — і JSON якраз один із найтиповіших варіантів.
2. Структура JSON: контейнери і примітиви
JSON тримається на дуже простій ідеї: є контейнери (складені типи), які можуть містити інші значення, і є примітиви (атомарні значення). Це як коробки та предмети. Коробка може містити інші коробки й предмети, а предмет усередині себе вже нічого не зберігає.
У JSON лише два контейнери: object і array. І лише чотири «прості» типи значень: string, number, bool, null. Якщо ви це запам’ятаєте — ви вже на півдорозі до спокійного життя.
Для наочності — маленька таблиця:
| Категорія | Вигляд у JSON | Приклад |
|---|---|---|
| Object (об’єкт) | |
|
| Array (масив) | |
|
| String (рядок) | |
|
| Number (число) | |
|
| Bool (логічне) | |
|
| 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" зазвичай фіксують як частину контракту формату.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ