SwiftJuniorTechnical

Что такое Codable в Swift и как настраивать кодирование/декодирование?

Codable = Encodable & Decodable. Swift генерирует реализацию автоматически; для кастомизации используют CodingKeys, init(from:) и encode(to:). JSONDecoder/JSONEncoder — основные точки входа.

Что такое Codable

Codable — это псевдоним (typealias) для пары протоколов Encodable & Decodable, введённых в Swift 4. Компилятор автоматически синтезирует соответствие, если все хранимые свойства сами являются Codable. Это позволяет сериализовать структуры и классы в JSON, Plist или любой другой формат без написания boilerplate-кода.

Базовый пример

import Foundation

struct User: Codable {
    let id: Int
    let name: String
    let email: String
}

// Декодирование из JSON
let json = """
{
  "id": 42,
  "name": "Alice",
  "email": "alice@example.com"
}
""".data(using: .utf8)!

let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: json)
print(user.name) // Alice

// Кодирование обратно в JSON
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encoded = try encoder.encode(user)
print(String(data: encoded, encoding: .utf8)!)

Кастомизация через CodingKeys

Если имена свойств Swift не совпадают с ключами JSON (например, сервер возвращает snake_case), используют вложенный enum CodingKeys:

struct Article: Codable {
    let id: Int
    let title: String
    let publishedAt: Date
    let authorName: String

    enum CodingKeys: String, CodingKey {
        case id
        case title
        case publishedAt = "published_at"
        case authorName  = "author_name"
    }
}

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
// Теперь published_at -> publishedAt автоматически

Альтернатива — задать decoder.keyDecodingStrategy = .convertFromSnakeCase, тогда CodingKeys вообще не нужен, если все имена укладываются в конвенцию.

Ручная реализация init(from:) и encode(to:)

Когда нужна нетривиальная логика (объединить два поля, вычислить значение, обработать полиморфизм), реализуют протоколы вручную:

struct Point: Codable {
    var x: Double
    var y: Double

    // JSON выглядит как [1.0, 2.0] — массив, не объект
    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        x = try container.decode(Double.self)
        y = try container.decode(Double.self)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try container.encode(x)
        try container.encode(y)
    }
}

let data = "[3.5, 7.2]".data(using: .utf8)!
let point = try JSONDecoder().decode(Point.self, from: data)
print(point.x, point.y) // 3.5 7.2

Полиморфизм и вложенные контейнеры

Реальные API часто возвращают тип-дискриминатор. Стандартный паттерн:

enum Shape: Codable {
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)

    private enum CodingKeys: String, CodingKey { case type, radius, width, height }

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        let type = try c.decode(String.self, forKey: .type)
        switch type {
        case "circle":
            let r = try c.decode(Double.self, forKey: .radius)
            self = .circle(radius: r)
        case "rectangle":
            let w = try c.decode(Double.self, forKey: .width)
            let h = try c.decode(Double.self, forKey: .height)
            self = .rectangle(width: w, height: h)
        default:
            throw DecodingError.dataCorruptedError(
                forKey: .type, in: c,
                debugDescription: "Unknown shape: \(type)"
            )
        }
    }

    func encode(to encoder: Encoder) throws {
        var c = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case .circle(let r):
            try c.encode("circle", forKey: .type)
            try c.encode(r, forKey: .radius)
        case .rectangle(let w, let h):
            try c.encode("rectangle", forKey: .type)
            try c.encode(w, forKey: .width)
            try c.encode(h, forKey: .height)
        }
    }
}

Настройки JSONDecoder и JSONEncoder

let decoder = JSONDecoder()
decoder.keyDecodingStrategy    = .convertFromSnakeCase
decoder.dateDecodingStrategy   = .iso8601
decoder.dataDecodingStrategy   = .base64

let encoder = JSONEncoder()
encoder.keyEncodingStrategy    = .convertToSnakeCase
encoder.dateEncodingStrategy   = .iso8601
encoder.outputFormatting       = [.prettyPrinted, .sortedKeys]

Подводные камни

  • Отсутствующий ключ vs null. decode(_:forKey:) бросает ошибку, если ключ отсутствует. Для опциональных полей используйте decodeIfPresent, иначе весь объект провалится с keyNotFound.
  • Смешение стратегий ключей. Если задать .convertFromSnakeCase и одновременно написать CodingKeys с явными строками, стратегия применяется поверх маппинга — поведение становится непредсказуемым.
  • Дата без стратегии. По умолчанию JSONDecoder ожидает Double (Unix timestamp). Забытый dateDecodingStrategy = .iso8601 даёт typeMismatch в рантайме, а не на этапе компиляции.
  • Enum с ассоциированными значениями. Swift не синтезирует Codable для enum с ассоциированными значениями автоматически (до Swift 5.5 включительно) — нужна ручная реализация или сторонние библиотеки.
  • Производительность на больших данных. JSONDecoder строит промежуточное дерево (JSONValue) перед маппингом. На массивах из сотен тысяч объектов это заметно; рассмотрите потоковую обработку или Codable + CoreData batch insert.
  • Скрытая потеря данных. Если сервер добавил новое поле, а в модели его нет, оно молча игнорируется. Нет механизма «strict» декодирования из коробки — дополнительные ключи не вызывают ошибку.
  • Circular references. Codable не умеет в циклические графы объектов — получите бесконечную рекурсию или переполнение стека. Нужно разрывать цикл вручную.
  • Синтез не работает с @propertyWrapper без поддержки Codable. Если обёртка не реализует Codable, автосинтез ломается со сложным сообщением об ошибке, не указывающим на обёртку.

Common mistakes

  • Сводить «Codable в Swift и как настраивать кодирование/декодирование» к синтаксису и не объяснять система типов.
  • Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии swift-20.
  • Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.

What the interviewer is testing

  • Формулирует точную модель для «Codable в Swift и как настраивать кодирование/декодирование» и подтверждает ее корректным примером.
  • Умеет связать ответ с ARC, тестированием и отладкой на устройстве.
  • Называет ограничения подхода swift-20, включая производительность, память и сопровождение.

Sources

Related topics