Зачем это нужно
Приложения редко живут в вакууме: они сохраняют данные на диск, отправляют их на сервер или получают ответ от 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, если данные некорректны или неполны.