Avatar ·

Как сделать enum Decodable в Swift: полное руководство

Всем привет! Я начинающий iOS-разработчик, и у меня возникла проблема с декодированием JSON в enum. У меня есть такой enum для статуса заказа: ```swift enum OrderStatus: String { case pending case confirmed case shipped case delivered case cancelled } ``` И я получаю JSON от сервера: ```json { "id": 1234, "status": "pending" } ``` Я пытаюсь декодировать это с помощью JSONDecoder: ```swift struct Order: Decodable { let id: Int let status: OrderStatus } ``` Но Xcode выдаёт ошибку: "Type 'OrderStatus' does not conform to protocol 'Decodable'" Я уже добавил String как rawValue для enum, думал, что это поможет. Но нет. Что я делаю не так? Нужно ли писать свою реализацию init(from:) или есть более простой способ? Ещё интересно, как быть, если сервер присылает не строку, а число (например, 0 - pending, 1 - confirmed), или если в JSON приходит неизвестное значение, которого нет в enum? Заранее спасибо за помощь! <<>>

Основное решение: конформность Decodable для String-based enum

Ваша проблема в том, что хотя OrderStatus имеет rawValue типа String, этого недостаточно для автоматической конформности Decodable. Нужно явно указать, что enum соответствует протоколу Decodable (или Codable):

enum OrderStatus: String, Decodable {
    case pending
    case confirmed
    case shipped
    case delivered
    case cancelled
}

Всё! Swift автоматически сгенерирует реализацию init(from:), используя rawValue типа String. Теперь JSONDecoder сможет декодировать строку "pending" в OrderStatus.pending.

Что если сервер присылает числа вместо строк?

Если сервер использует числовые коды (например, 0, 1, 2), вам нужно либо изменить rawValue на Int, либо написать кастомную реализацию:

enum OrderStatus: Int, Decodable {
    case pending = 0
    case confirmed = 1
    case shipped = 2
    case delivered = 3
    case cancelled = 4
}

Как обработать неизвестные значения (fallback)?

Часто сервер может прислать новое значение, которого ещё нет в enum. Чтобы избежать краша, добавьте специальный кейс unknown и реализуйте кастомную инициализацию:

enum OrderStatus: String, Decodable {
    case pending
    case confirmed
    case shipped
    case delivered
    case cancelled
    case unknown
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let rawString = try container.decode(String.self)
        self = OrderStatus(rawValue: rawString) ?? .unknown
    }
}

Этот подход гарантирует, что приложение не упадёт, если сервер пришлёт, например, "refunded".

Альтернатива: использование CodingKeys для сложных JSON

Если JSON имеет другую структуру (например, статус внутри вложенного объекта), используйте CodingKeys:

struct Order: Decodable {
    let id: Int
    let status: OrderStatus
    
    enum CodingKeys: String, CodingKey {
        case id
        case status = "order_status_code"
    }
}

Продвинутый случай: enum с ассоциированными значениями

Если ваш enum содержит ассоциированные значения, автоматическая декодировка не сработает. Нужно писать полную реализацию:

enum ServerResponse: Decodable {
    case success(data: [String: Any])
    case error(code: Int, message: String)
    
    enum CodingKeys: String, CodingKey {
        case type
        case data
        case code
        case message
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let type = try container.decode(String.self, forKey: .type)
        
        switch type {
        case "success":
            let data = try container.decode([String: Any].self, forKey: .data)
            self = .success(data: data)
        case "error":
            let code = try container.decode(Int.self, forKey: .code)
            let message = try container.decode(String.self, forKey: .message)
            self = .error(code: code, message: message)
        default:
            throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown type: \(type)")
        }
    }
}

Итоговые рекомендации

  • Для простых String/Int enum — просто добавьте Decodable в объявление
  • Для fallback-значений — используйте кастомный init(from:) с оператором ??
  • Для ассоциированных значений — пишите полную реализацию декодирования
  • Не забывайте про Error handling: обрабатывайте DecodingError при вызове decode
<<>>

Использование протокола RawRepresentable для автоматической декодировки

На самом деле, существует ещё более элегантный способ, если ваш enum уже соответствует RawRepresentable (а все enum с rawValue соответствуют). Можно добавить конформность через extension:

enum OrderStatus: String {
    case pending
    case confirmed
    case shipped
    case delivered
    case cancelled
}

extension OrderStatus: Decodable {} // Всё! Работает автоматически

Почему это работает? Потому что RawRepresentable с rawValue типа String или Int уже имеет всю необходимую информацию для декодирования. Swift автоматически синтезирует реализацию init(from:), используя rawValue.

Дополнительный совет: обработка опциональных значений

Если сервер может не присылать статус вообще, сделайте поле опциональным:

struct Order: Decodable {
    let id: Int
    let status: OrderStatus? // Опционально
}

Также можно использовать decodeIfPresent в кастомной реализации:

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    status = try container.decodeIfPresent(OrderStatus.self, forKey: .status)
}

Этот подход особенно полезен, когда API развивается и некоторые поля могут отсутствовать в ответе.