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[Book JSON Object] --> B["id: number"]
A --> C["title: string"]
A --> D["authors: array"]
D --> E["author: string"]
A --> F["meta: object"]
F --> G["addedBy: string"]
F --> H["isFavorite: bool"]
7. Схема и дизайн хранения для LibraryCLI
Что такое схема и почему без неё больно
Вот здесь начинается взрослая жизнь. Сама по себе строка JSON не гарантирует вам почти ничего, кроме того, что это валидный JSON. Но для программы важнее другое: совпадает ли структура данных с тем, что мы ожидаем.
Схема (в нашем учебном смысле) — это договорённость о четырёх вещах:
1) какой тип у корня (объект {} или массив []),
2) какие ключи у объектов и как они называются,
3) какие типы у значений (строка/число/массив/объект…),
4) какие поля обязательны, а какие могут отсутствовать.
Почему это важно? Потому что Swift строгий: если вы написали, что у книги есть id: Int, то вы внутренне пообещали себе, что из данных всегда можно получить целое число. А JSON вам ничего такого не обещал. Именно это расхождение — ключевой источник ошибок и головной боли при работе с реальными данными.
Небольшая аналогия: JSON без схемы — это «посылка без описи вложения». Вроде коробка приехала, но что внутри — узнаете только вскрыв. А когда вскрыли — может оказаться, что там не книга, а керамическая кружка, и вы уже потрясли коробку по пути (в коде это называется fatalError, но мы сегодня добрые).
«Нет значения»: ключ отсутствует vs ключ есть, но 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 vs «почти 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" обычно фиксируются как часть контракта формата.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ