1. Зачем нужен Codable
Когда вы только начинаете программировать, кажется, что данные всегда живут в переменных: let title = "...", var items: [Book] = .... Но как только вы захотите сохранить коллекцию книг между запусками программы или передать её куда-то наружу, придётся выйти из уютного мира типов Swift в мир «просто данных». JSON — один из самых популярных форматов: он человекочитаемый, текстовый и понятный почти всем языкам.
Проблема в том, что JSON по природе не такой строгий, как Swift. Он не знает, что такое Book, ReadingStatus или «поле обязательно, а поле опционально». Swift же очень любит конкретику: «вот Int, вот String, вот Optional<String>». Чтобы эти два мира не поссорились, Swift предлагает стандартизированный механизм сериализации: Encodable / Decodable / Codable. И главное удобство — компилятор часто умеет сам написать за вас код «упаковки/распаковки», то есть сделать синтез.
2. Протоколы и правила синтеза
Encodable, Decodable и Codable
Если читать названия, всё звучит логично, но на практике новички часто путаются: «Я кодирую в JSON или декодирую из JSON?». Давайте разложим это «по полочкам», без перегруза терминами.
Encodable означает: тип умеет кодировать себя во внешний формат, то есть превращаться в представление, которое сможет записать encoder.
Decodable означает обратное: тип можно восстановить из внешнего представления, то есть decoder может собрать значение типа из данных.
В стандартной библиотеке Swift Codable — это просто удобное имя для комбинации этих двух протоколов: Encodable & Decodable.
Чаще всего в прикладном коде (и в нашем учебном приложении) вы будете писать именно Codable, потому что для хранения данных обычно нужно и «записать», и «прочитать».
Пример
struct Ping: Codable {
let message: String
let code: Int
}
Здесь мы не написали ни одной строки «про JSON». Мы просто сказали: Ping умеет быть и закодированным, и декодированным. А как именно — зависит от конкретного encoder/decoder (например, JSON).
Что участвует в синтезе: stored properties
Перед тем как радоваться синтезу, нужно понять простое правило: компилятор «видит данные» там, где у типа есть stored properties — реальные поля, которые хранятся в памяти. Это те самые let title: String, var tags: [String] и т.д.
А вот computed properties (например, var displayTitle: String { ... }) — это не хранимые данные, а вычисляемый результат. И обычно они не должны сериализоваться, потому что их можно восстановить из других полей. Иначе вы рискуете хранить в JSON «то же самое, но в другой упаковке», а потом ловить рассинхронизацию.
Пример
struct BookPreview: Codable {
let title: String
let author: String
var fullTitle: String {
"\(title) — \(author)"
}
}
С точки зрения данных, BookPreview хранит только title и author. fullTitle не хранится, а вычисляется. Поэтому синтез кодирования/декодирования будет опираться на stored properties и спокойно проигнорирует computed property.
Если провести аналогию: stored property — это «ингредиенты», computed property — «готовое блюдо». В холодильник мы кладём ингредиенты, а не фотографию борща (хотя некоторые пытаются).
Условия автосинтеза Codable
Теперь к самой приятной части: когда именно компилятор готов «поработать стажёром» и сгенерировать реализацию encode(to:) и init(from:) за нас.
Главное правило звучит так: все stored properties типа должны сами быть Encodable/Decodable. Именно это и описано в базовом дизайне механизма: если свойства кодируемые/декодируемые, то и сам тип может получить автоматически сгенерированную реализацию.
Пример
struct User: Codable {
let id: Int
let name: String
let isActive: Bool
}
Поля простые: Int, String, Bool. Всё хорошо.
А вот тут уже начинается цепочка зависимостей:
Пример
struct Author: Codable {
let name: String
}
struct Book: Codable {
let title: String
let author: Author
}
Book может быть Codable, потому что Author тоже Codable. Это похоже на проверку «входит ли каждый пассажир с билетом»: чтобы весь автобус поехал, у всех должны быть билеты.
Как Swift-типы обычно ложатся на JSON
Важно зафиксировать: Codable — это способность кодироваться/декодироваться вообще, а JSON — конкретный формат. Но на практике мы чаще всего говорим про связку Codable + JSONEncoder/JSONDecoder.
Ниже — ориентир, который помогает новичкам не запутаться в моделировании.
| Идея данных | JSON-форма | Тип Swift (часто) | Обычно синтезируется? |
|---|---|---|---|
| Текст | |
|
Да |
| Целое число | |
|
Да |
| Булево | |
|
Да |
| «Может отсутствовать» | ключ может отсутствовать / быть null | |
Да, если кодируемый |
| Список | |
|
Да, если кодируемый |
| Объект | |
|
Да, если поля кодируемые |
| Ограниченный набор значений | |
|
Да (часто идеально) |
Эта таблица не «спека», а скорее практический компас: если вы моделируете книгу, то title: String, year: Int, tags: [String] — почти всегда хороший старт.
Optional в модели
Optional — одна из тех вещей в Swift, которые сначала раздражают, потом спасают, а потом вы начинаете раздражаться на языки, где Optional нет (но это уже философия).
Когда свойство Optional, вы явно говорите: «это поле может не иметь значения». И это идеально ложится на реальность JSON: в данных иногда нет ключа, иногда значение null, иногда поле есть только в новых версиях данных.
С точки зрения синтеза правило простое: если у вас subtitle: String?, то синтез Codable обычно работает так же легко, как и с обычными типами, потому что Optional сам поддерживается системой.
Пример
struct Book: Codable {
let title: String
let subtitle: String?
}
Здесь важно дисциплинировать себя как разработчика: Optional — это не «давайте сделаем всё optional, чтобы меньше думать». Это договорённость модели: поле правда может отсутствовать. Иначе вы теряете смысл строгих типов и возвращаетесь в мир «строка может быть пустой, а может быть null, а может быть "что-то ещё"».
Коллекции
Коллекции — это то, без чего реальные данные не живут. Наша библиотека книг почти наверняка будет хранить список. Хорошая новость: [T] становится Codable, если T тоже Codable. Это идеально сочетается с синтезом: вы можете спокойно делать var items: [Book].
Пример
struct Shelf: Codable {
let name: String
var books: [String]
}
Важный нюанс для начинающих: синтез не делает «магии». Он не умеет «догадаться», как кодировать тип, который сам не Codable. Поэтому если вы сделаете массив books: [Book], то Book обязан быть Codable. В этом месте компилятор очень честный: либо вся цепочка кодируема, либо нет.
enum и JSON
enum — лучший способ не хранить «магические строки» вроде "planned", "finished", "in_progress_please_dont_typo". В JSON это всё равно обычно будет строкой, но в Swift мы хотим строгий набор значений.
Самый дружелюбный к синтезу и JSON вариант: enum SomeState: String, Codable. Тогда в JSON уйдёт строка raw value, а в Swift вы не сможете случайно записать «не тот статус».
Пример
enum ReadingStatus: String, Codable {
case planned
case reading
case finished
}
Это выглядит почти как чит-код (простите за каламбур): вы одновременно получаете и ограничения, и удобство сериализации.
3. Когда синтез ломается и как это чинить
Типовые причины поломки синтеза
Синтез — штука отличная, но он не обязан работать всегда. И это нормально: компилятор не должен угадывать ваши намерения. Он либо может гарантированно сгенерировать корректный код, либо просит вас вмешаться.
Самая частая причина поломки звучит скучно, зато правдиво: среди stored properties есть тип, который не Codable.
Представьте, вы добавили в модель «какую-нибудь удобную штуку». Например, замыкание для сортировки (потому что «хочу красиво»). С точки зрения сериализации это бессмыслица: как вы запишете функцию в JSON?
Пример, который НЕ скомпилируется (идея проблемы)
struct BadBook: Codable {
let title: String
let sorter: (String, String) -> Bool
}
Компилятор в этом месте абсолютно прав: «Я не буду притворяться, что могу это закодировать».
Как вернуться в «зелёную зону» без ручного кодирования
Как чинить такие ситуации на нашем уровне курса (без ухода в ручное кодирование):
Вы либо удаляете такое поле из stored properties, либо превращаете его в computed property, либо храните вместо сложного типа простое представление (например, не «функцию сортировки», а sortMode: String или enum SortMode: String, Codable).
Пример (замена “сложного” на “хранимое”)
enum SortMode: String, Codable {
case byTitle
case byYear
}
struct GoodBookConfig: Codable {
let sortMode: SortMode
}
Это как в жизни: вместо того чтобы хранить «как именно я буду думать завтра», вы храните «какое решение я принял».
Мини-тест: функция, которая принимает только Codable
Иногда хочется «пощупать руками»: а мой тип правда Codable? Но мы ещё не кодировали в JSON, так что давайте сделаем проверку без JSONEncoder.
Фокус простой: напишем функцию, которая принимает только T: Codable. Если ваш тип не Codable, компилятор не даст его передать.
Пример
func acceptsCodable<T: Codable>(_ value: T) {
print("Тип подходит под Codable")
}
struct Note: Codable {
let text: String
}
acceptsCodable(Note(text: "Hello"))
// Тип подходит под Codable
Это «компиляторный юнит-тест» на минималках: если код собрался, значит хотя бы на уровне синтеза у вас всё хорошо.
4. Модели для приложения LibraryCLI
Готовим Book к Codable
В нашем учебном приложении мы постепенно идём к тому, чтобы хранить библиотеку книг не только в памяти, но и в файле. Сегодня (в рамках темы синтеза) нам важно сделать типы такими, чтобы они вообще могли быть сериализованы без ручной работы.
Начнём с простой модели. Обратите внимание: мы избегаем «сложных» полей, которые требуют тонкой настройки (например, хитрых дат). Храним только то, что однозначно представляется в JSON.
Пример
struct Book: Codable {
let id: Int
let title: String
let author: String
let year: Int
}
Теперь усложним: добавим теги и статус чтения. Всё ещё остаёмся в мире «синтезируется легко».
Пример
enum ReadingStatus: String, Codable {
case planned
case reading
case finished
}
struct Book: Codable {
let id: Int
let title: String
let author: String
let year: Int
let tags: [String]
let status: ReadingStatus
}
Если вы чувствуете приятное облегчение — это нормально. Это тот редкий момент, когда строгая типизация и сериализация не дерутся, а дружат.
Корневой контейнер: почему не [Book]
На уровне формата хранения очень важно иметь место для служебной информации: версия схемы, возможно, дополнительные настройки и т.д. Если корень — просто массив, вам некуда положить метаданные. Поэтому практичный подход: сделать корнем объект-контейнер.
Сейчас мы создадим такой контейнер, но без чтения/записи на диск — только модель. Мы как бы строим «коробку», в которую позже будем складывать книги.
Пример
struct LibraryFile: Codable {
let schemaVersion: Int
var items: [Book]
}
Здесь синтез работает по тем же правилам: schemaVersion — Int, items — [Book], а Book — Codable. Значит LibraryFile тоже автоматически становится Codable. Это как матрёшка: если внутренняя кукла в порядке, то и внешняя собирается.
5. Что компилятор синтезирует
Общая схема синтеза
Важно понимать, что синтез — это не «магическое поле Codable = true». На самом деле у Encodable и Decodable есть конкретные требования: метод encode(to:) и инициализатор init(from:). Компилятор может сгенерировать их автоматически, если структура данных однозначна.
Можно представить это так:
flowchart TD
A["Ваш тип (struct/enum) с stored properties"] --> B{"Все stored properties тоже Codable?"}
B -- "Да" --> C["Компилятор синтезирует encode(to:) и init(from:)"]
B -- "Нет" --> D["Нужно менять модель или писать вручную"]
В этой лекции мы сознательно держимся ветки «Да»: проектируем модели так, чтобы синтез работал.
Имена свойств становятся ключами
Даже если мы сегодня не пишем CodingKeys и не настраиваем маппинг, полезно знать идею: при синтезе компилятор связывает имена свойств с ключами в «ключевом контейнере». В базовой модели это означает: title становится ключом "title", schemaVersion становится "schemaVersion" и т.д. Сама идея «ключи соответствуют свойствам» заложена в механизме синтеза.
Отсюда практическое правило: называйте свойства осмысленно и стабильно, потому что имя свойства становится частью формата данных. Если вы сегодня назвали поле items, а завтра переименовали в books, то старые данные могут перестать читаться без дополнительных усилий.
6. Типичные ошибки при синтезе Codable
Ошибка №1: пытаться сериализовать «поведение», а не данные.
Самый популярный провал — добавить в модель замыкания, ссылки на сервисы, какие-то «хелперы для сортировки/валидирования» и ожидать, что Codable это проглотит. Сериализация работает с данными, а не с логикой. Лечится это тем, что вы храните описание поведения (например, enum SortMode), а поведение строите из него в коде.
Ошибка №2: считать, что computed properties тоже попадут в JSON.
Иногда студент пишет computed property fullTitle, потом удивляется: «Почему в JSON нет fullTitle?». Потому что computed property — не данные, а вычисление. Обычно это как раз плюс: меньше дублирования, меньше риска рассинхрона.
Ошибка №3: делать все поля Optional, чтобы “точно декодировалось”.
Да, если всё optional, то «шансов упасть меньше». Но вы при этом теряете смысл модели: где обязательные данные? где допустимы пропуски? В итоге ошибки не исчезают — они просто переезжают из decode в вашу бизнес-логику, где вы начинаете писать if let title = title { ... } на каждом шаге.
Ошибка №4: забыть протянуть Codable по цепочке вложенных типов.
Очень частая ситуация: Book сделали Codable, внутри добавили Author, но Author забыли сделать Codable. Ошибка компиляции кажется «громкой», но она честная: пока все детали не кодируемы, целое не кодируется.
Ошибка №5: переименовать свойства и случайно поменять формат данных.
Переименование items → books или schemaVersion → version кажется безобидным рефакторингом. Но если у вас уже есть сохранённые данные, это может стать «тихой поломкой совместимости». Даже в учебном проекте полезно вырабатывать привычку: имена в модели данных — часть контракта.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ