Как сделать enum Decodable в Swift: полное руководство
Основное решение: конформность 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:)с оператором?? - Для ассоциированных значений — пишите полную реализацию декодирования
- Не забывайте про
Errorhandling: обрабатывайте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 развивается и некоторые поля могут отсутствовать в ответе.
Ответов пока нет.
Войдите чтобы оставить ответ