Codable для Swift: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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))

Рекомендаций