JavaRush /Курсы /Swift SELF /Codable — синтез Encodable и Decodable в Swift

Codable — синтез Encodable и Decodable в Swift

Swift SELF
58 уровень, 1 лекция
Открыта

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 (часто) Обычно синтезируется?
Текст
"hello"
String
Да
Целое число
42
Int
Да
Булево
true
Bool
Да
«Может отсутствовать» ключ может отсутствовать / быть null
Optional<T>
Да, если
T
кодируемый
Список
[...]
[T]
Да, если
T
кодируемый
Объект
{ "k": ... }
struct
Да, если поля кодируемые
Ограниченный набор значений
"planned"
enum: String
Да (часто идеально)

Эта таблица не «спека», а скорее практический компас: если вы моделируете книгу, то 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]
}

Здесь синтез работает по тем же правилам: schemaVersionInt, items[Book], а BookCodable. Значит 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: переименовать свойства и случайно поменять формат данных.
Переименование itemsbooks или schemaVersionversion кажется безобидным рефакторингом. Но если у вас уже есть сохранённые данные, это может стать «тихой поломкой совместимости». Даже в учебном проекте полезно вырабатывать привычку: имена в модели данных — часть контракта.

1
Задача
Swift SELF, 58 уровень, 1 лекция
Недоступна
Пинг сервиса
Пинг сервиса
1
Задача
Swift SELF, 58 уровень, 1 лекция
Недоступна
Визитка контакта
Визитка контакта
1
Задача
Swift SELF, 58 уровень, 1 лекция
Недоступна
Каталог чтения
Каталог чтения
1
Задача
Swift SELF, 58 уровень, 1 лекция
Недоступна
Режим сортировки
Режим сортировки
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ