$ sudo teach IT

Зачем это нужно

Приложения редко живут в вакууме: они сохраняют данные на диск, отправляют их на сервер или получают ответ от API. Но struct и class существуют только в памяти вашей программы — это объекты Swift, а не текст. Чтобы передать данные куда-то ещё, их нужно превратить в универсальный формат, который поймёт любая система: чаще всего это JSON. И наоборот — полученный откуда-то JSON нужно превратить обратно в удобные типы Swift, а не ковыряться в тексте руками. Именно эту пару задач — «упаковать» и «распаковать» — в Swift решает Codable.

Encodable, Decodable и Codable

В стандартной библиотеке есть два протокола. Encodable — тип умеет кодировать себя во внешнее представление (например, в JSON). Decodable — тип умеет, наоборот, воссоздавать себя из такого представления. Codable — это просто удобное имя для «оба сразу»: typealias Codable = Encodable & Decodable.

Самое приятное: если все хранимые свойства структуры сами соответствуют Codable (а String, Int, Double, Bool, массивы и опционалы этих типов — соответствуют из коробки), компилятор Swift сам сгенерирует нужный код при объявлении соответствия. Писать ничего вручную не нужно.

import Foundation

struct Product: Codable {
    let name: String
    let price: Double
    let inStock: Bool
}

Здесь компилятор увидел, что String, Double и Bool кодируемые, и молча создал реализацию Encodable и Decodable для Product. Если хотя бы одно свойство окажется некодируемым типом, компилятор откажется собирать проект и укажет на проблемное свойство.

JSONEncoder и JSONDecoder

Сам протокол Codable не знает про JSON — он описывает общий контракт «кодируется / декодируется». Конкретный формат задаёт кодировщик. Для JSON в Foundation есть пара классов: JSONEncoder превращает значение в Data, JSONDecoder — наоборот.

let product = Product(name: "Клавиатура", price: 45.5, inStock: true)

let encoder = JSONEncoder()
let data = try encoder.encode(product)
let text = String(data: data, encoding: .utf8)!
print(text)

encode помечен как throws: кодирование почти никогда не падает для обычных структур, но компилятор всё равно требует пометки, ведь в общем случае ошибка возможна (например, если свойство — число с NaN). Результат — Data, набор байтов; чтобы посмотреть его как текст, мы превращаем в String.

let json = "{\"name\":\"Мышь\",\"price\":19.9,\"inStock\":false}"
let decoder = JSONDecoder()
let restored = try decoder.decode(Product.self, from: Data(json.utf8))
print(restored.name)

decode принимает тип, который нужно получить (Product.self), и данные. Он тоже throws: если JSON битый или не хватает поля, декодер бросит ошибку вместо того, чтобы вернуть мусор. На практике при разборе часто применяют try?, чтобы получить опционал вместо падения программы:

let broken = "не json совсем"
let result = try? decoder.decode(Product.self, from: Data(broken.utf8))
print(result == nil)

Свои CodingKeys

Названия полей в JSON нередко не совпадают со стилем именования Swift: сервер присылает in_stock в snake_case, а в Swift принято inStock в camelCase. Для этого есть вложенный enum CodingKeys, соответствующий протоколу CodingKey: он говорит компилятору, каким внешним ключом кодировать каждое свойство.

struct Item: Codable {
    let title: String
    let inStock: Bool

    enum CodingKeys: String, CodingKey {
        case title
        case inStock = "in_stock"
    }
}

Важное правило: если вы объявили свой CodingKeys, в нём должны быть перечислены ВСЕ хранимые свойства, которые нужно кодировать, — те, что забыли включить, компилятор не сможет ни закодировать, ни декодировать, и сборка упадёт с ошибкой о несоответствии протоколу.

Вложенные структуры

Codable прекрасно работает рекурсивно: если структура содержит поле другого типа, который тоже Codable, кодирование и декодирование пройдут насквозь, без дополнительного кода.

struct Address: Codable {
    let city: String
    let zipCode: String

    enum CodingKeys: String, CodingKey {
        case city
        case zipCode = "zip_code"
    }
}

struct Customer: Codable {
    let fullName: String
    let address: Address

    enum CodingKeys: String, CodingKey {
        case fullName = "full_name"
        case address
    }
}

При кодировании Customer декодер сам спустится в Address и обработает его по тем же правилам. Получится вложенный JSON-объект внутри JSON-объекта — ровно так, как обычно выглядят ответы настоящих API.

Настройка вывода: outputFormatting

По умолчанию JSONEncoder печатает компактный JSON в одну строку, и порядок ключей в нём не гарантирован — кодировщик не обязан сохранять порядок объявления свойств. Если нужен читаемый многострочный вывод (например, для логов), у энкодера есть свойство outputFormatting:

let prettyEncoder = JSONEncoder()
prettyEncoder.outputFormatting = [.prettyPrinted, .sortedKeys]

.prettyPrinted добавляет отступы и переводы строк, .sortedKeys сортирует ключи по алфавиту — это удобно, если вам всё же важен стабильный, предсказуемый порядок в тексте.

Частые ошибки

  • Забыть import Foundation — без него компилятор не увидит ни JSONEncoder, ни JSONDecoder, ни Data.
  • Перечислить в своём CodingKeys не все свойства структуры — компилятор откажется собирать код, а сообщение об ошибке не всегда явно укажет на пропущенный случай.
  • Сравнивать закодированный JSON как строку целиком. Порядок ключей без .sortedKeys не гарантирован, поэтому одна и та же структура может дать разный текст при разных запусках. Правильнее декодировать результат обратно и сравнивать значения.
  • Забыть, что encode и decode — это throws-функции: их нужно вызывать через try, try? или try!, а не как обычные функции.

Резюме

  • Encodable и Decodable — протоколы кодирования и декодирования, Codable — их объединение.
  • Если все свойства сами Codable, соответствие можно объявить без единой строчки дополнительного кода.
  • JSONEncoder превращает значение в Data, JSONDecoder — Data обратно в значение; оба метода бросают ошибки.
  • Свой enum CodingKeys: String, CodingKey позволяет задать другие внешние имена полей, но обязан перечислять все свойства.
  • Вложенные Codable-структуры кодируются и декодируются рекурсивно сами по себе.
  • outputFormatting у энкодера управляет читаемостью и порядком ключей в получившемся тексте.

Проверьте себя

4 вопроса

Кодируем и декодируем заметку

Определите структуру Note, соответствующую Codable и Equatable, с двумя полями: title типа String и done типа Bool.

Реализуйте две функции.

encodeNote(_ note: Note) -> String — кодирует значение Note в JSON-текст с помощью JSONEncoder и возвращает получившуюся строку.

decodeNote(_ json: String) -> Note? — пытается декодировать строку как Note с помощью JSONDecoder. Если строка не является валидным JSON или в ней не хватает обязательного поля, функция должна вернуть nil, а не упасть с ошибкой.

Вложенные данные и свои имена ключей

Определите структуры Address и Person, соответствующие Codable и Equatable.

Address хранит city: String и zipCode: String, причём во внешнем JSON поле zipCode должно называться zip_code.

Person хранит fullName: String и address: Address, причём во внешнем JSON поле fullName должно называться full_name. Используйте для этого свои CodingKeys в обеих структурах.

Реализуйте функции.

personToJSON(_ person: Person) -> String — кодирует Person в JSON-текст.

personFromJSON(_ json: String) -> Person? — декодирует JSON-текст обратно в Person, возвращая nil, если данные некорректны или неполны.