Codable для Swift: полное руководство с примерами
1. Что это и зачем нужна
Codable — это встроенный протокол (typealias от Encodable & Decodable) в языке Swift, представленный в Swift 4. Он предоставляет декларативный способ сериализации и десериализации пользовательских типов данных в форматы, такие как JSON, Property List (plist) и другие. Вместо того чтобы вручную писать громоздкий код для преобразования структур в словари и обратно, Codable позволяет автоматически генерировать логику кодирования/декодирования на основе свойств вашей модели.
Зачем это нужно? В современной разработке под iOS/macOS практически каждое приложение взаимодействует с API сервера, получая и отправляя данные в формате JSON. Без Codable разработчику пришлось бы вручную писать циклы, проверять типы, обрабатывать опциональные значения и писать обратный парсер. Codable решает эту проблему, делая код чище, безопаснее и значительно сокращая время разработки. Он также является стандартом де-факто для работы с JSON в экосистеме Apple.
Codable работает на основе протоколов Encodable (для преобразования из Swift-типа в данные) и Decodable (для преобразования из данных в Swift-тип). Комбинируя их, вы получаете полный цикл сериализации. Библиотека тесно интегрирована с JSONEncoder и JSONDecoder, которые являются основными инструментами для работы с JSON.
2. Установка
Codable является частью стандартной библиотеки Swift (Foundation). Никакой дополнительной установки не требуется. Вам не нужно использовать pip, npm, CocoaPods или Swift Package Manager для его подключения. Просто импортируйте Foundation в вашем файле:
import Foundation
Если вы используете Swift в среде Linux (например, через Swift Package Manager), Codable также доступен из коробки. Для серверных фреймворков, таких как Vapor, Codable используется по умолчанию.
3. Быстрый старт — минимальный рабочий пример
Создадим простую структуру пользователя и сериализуем/десериализуем её в JSON.
import Foundation
// 1. Определяем модель, соответствующую протоколу Codable
struct User: Codable {
let id: Int
let name: String
let email: String
}
// 2. Создаем экземпляр
let user = User(id: 1, name: "Alice", email: "alice@example.com")
// 3. Кодируем в JSON
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted // Для красивого вывода
do {
let jsonData = try encoder.encode(user)
if let jsonString = String(data: jsonData, encoding: .utf8) {
print("JSON: \n\(jsonString)")
}
// 4. Декодируем обратно
let decoder = JSONDecoder()
let decodedUser = try decoder.decode(User.self, from: jsonData)
print("Decoded user: \(decodedUser.name), email: \(decodedUser.email)")
} catch {
print("Error: \(error)")
}
Вывод в консоль:
JSON:
{
"id" : 1,
"name" : "Alice",
"email" : "alice@example.com"
}
Decoded user: Alice, email: alice@example.com
Как видите, всего несколько строк кода — и сериализация готова.
4. Основные методы/функции/классы
Рассмотрим ключевые компоненты Codable, которые вы будете использовать постоянно.
4.1 JSONEncoder
Сигнатура: class JSONEncoder : NSCoder
Что делает: Преобразует экземпляры типов, соответствующих протоколу Encodable, в объект Data (JSON).
struct Product: Codable {
let title: String
let price: Double
}
let product = Product(title: "MacBook Pro", price: 2499.99)
let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys // Сортировка ключей
if let data = try? encoder.encode(product) {
print(String(data: data, encoding: .utf8)!)
}
// Вывод: {"price":2499.99,"title":"MacBook Pro"}
4.2 JSONDecoder
Сигнатура: class JSONDecoder : NSCoder
Что делает: Преобразует JSON-данные (Data) обратно в экземпляр указанного типа, соответствующего протоколу Decodable.
let jsonString = """
{"title":"iPhone 15","price":999.0}
"""
let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
if let phone = try? decoder.decode(Product.self, from: jsonData) {
print(phone.title) // iPhone 15
}
4.3 Encodable протокол
Сигнатура: protocol Encodable
Что делает: Объявляет, что тип может быть сериализован. Требует реализации метода encode(to:), но компилятор генерирует его автоматически, если все свойства также Codable.
struct Car: Encodable {
let brand: String
let year: Int
}
// Компилятор автоматически синтезирует encode(to:)
4.4 Decodable протокол
Сигнатура: protocol Decodable
Что делает: Объявляет, что тип может быть десериализован. Требует реализации инициализатора init(from:), который также синтезируется автоматически.
struct Book: Decodable {
let title: String
let author: String
}
// Компилятор автоматически синтезирует init(from:)
4.5 CodingKeys (enum)
Сигнатура: enum CodingKeys: String, CodingKey
Что делает: Позволяет переопределить соответствие между именами свойств Swift и ключами JSON (например, при использовании snake_case в API).
struct UserProfile: Codable {
let firstName: String
let lastName: String
enum CodingKeys: String, CodingKey {
case firstName = "first_name"
case lastName = "last_name"
}
}
let json = """
{"first_name":"John","last_name":"Doe"}
"""
let data = json.data(using: .utf8)!
let profile = try! JSONDecoder().decode(UserProfile.self, from: data)
print(profile.firstName) // John
4.6 JSONEncoder.KeyEncodingStrategy
Сигнатура: enum JSONEncoder.KeyEncodingStrategy
Что делает: Глобальная стратегия преобразования ключей. Позволяет автоматически конвертировать camelCase в snake_case и наоборот без написания CodingKeys.
struct User: Codable {
let fullName: String
let emailAddress: String
}
let user = User(fullName: "Alice", emailAddress: "a@b.com")
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try! encoder.encode(user)
print(String(data: data, encoding: .utf8)!)
// {"full_name":"Alice","email_address":"a@b.com"}
4.7 JSONDecoder.KeyDecodingStrategy
Сигнатура: enum JSONDecoder.KeyDecodingStrategy
Что делает: Обратная стратегия для декодера. Преобразует snake_case из JSON в camelCase в Swift.
let json = """
{"full_name":"Bob","email_address":"b@c.com"}
"""
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let data = json.data(using: .utf8)!
let user = try! decoder.decode(User.self, from: data)
print(user.fullName) // Bob
4.8 JSONEncoder.outputFormatting
Сигнатура: var outputFormatting: JSONEncoder.OutputFormatting
Что делает: Управляет форматированием выводимого JSON (например, отступы, сортировка ключей, без экранирования слэшей).
let data = ["key": "value"]
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
if let jsonData = try? encoder.encode(data) {
print(String(data: jsonData, encoding: .utf8)!)
}
// Вывод:
// {
// "key" : "value"
// }
4.9 JSONDecoder.dateDecodingStrategy
Сигнатура: var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy
Что делает: Определяет, как декодировать даты из JSON (ISO 8601, timestamp, custom formatter и т.д.).
struct Event: Codable {
let name: String
let date: Date
}
let json = """
{"name":"Conference","date":"2024-12-01T10:00:00Z"}
"""
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let data = json.data(using: .utf8)!
let event = try! decoder.decode(Event.self, from: data)
print(event.date) // 2024-12-01 10:00:00 +0000
4.10 JSONEncoder.dateEncodingStrategy
Сигнатура: var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy
Что делает: Определяет, как кодировать даты в JSON (аналогично декодированию).
let event = Event(name: "Workshop", date: Date())
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let data = try! encoder.encode(event)
print(String(data: data, encoding: .utf8)!)
// {"date":"2024-10-27T12:00:00Z","name":"Workshop"}
5. Реальный практический пример (100-200 строк кода)
Создадим приложение для работы с API GitHub. Мы будем получать список репозиториев пользователя и выводить их названия и количество звезд.
import Foundation
// MARK: - Модели данных
struct GitHubRepo: Codable {
let id: Int
let name: String
let fullName: String
let owner: Owner
let description: String?
let stargazersCount: Int
let forksCount: Int
let language: String?
let updatedAt: Date
enum CodingKeys: String, CodingKey {
case id, name, owner, description, language
case fullName = "full_name"
case stargazersCount = "stargazers_count"
case forksCount = "forks_count"
case updatedAt = "updated_at"
}
}
struct Owner: Codable {
let login: String
let avatarUrl: String
enum CodingKeys: String, CodingKey {
case login
case avatarUrl = "avatar_url"
}
}
// MARK: - Сетевой менеджер
class GitHubAPIManager {
static let shared = GitHubAPIManager()
private let baseURL = "https://api.github.com"
private let decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
func fetchRepos(for username: String, completion: @escaping (Result<[GitHubRepo], Error>) -> Void) {
let urlString = "\(baseURL)/users/\(username)/repos"
guard let url = URL(string: urlString) else {
completion(.failure(NSError(domain: "Invalid URL", code: 0)))
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NSError(domain: "No data", code: 1)))
return
}
do {
let repos = try self.decoder.decode([GitHubRepo].self, from: data)
completion(.success(repos))
} catch {
completion(.failure(error))
}
}.resume()
}
}
// MARK: - Использование
let manager = GitHubAPIManager.shared
manager.fetchRepos(for: "apple") { result in
switch result {
case .success(let repos):
print("Found \(repos.count) repositories:")
for repo in repos.prefix(5) { // Покажем первые 5
print("""
- \(repo.name)
Stars: \(repo.stargazersCount)
Language: \(repo.language ?? "N/A")
Updated: \(repo.updatedAt)
""")
}
case .failure(let error):
print("Error fetching repos: \(error.localizedDescription)")
}
}
// Для запуска в playground нужно разрешить сетевые запросы
// RunLoop.main.run(until: Date(timeIntervalSinceNow: 5))