Codable enum со стандартным случаем в Swift 4

Я определил enum следующим образом:

enum Type: String, Codable {
    case text = "text"
    case image = "image"
    case document = "document"
    case profile = "profile"
    case sign = "sign"
    case inputDate = "input_date"
    case inputText = "input_text"
    case inputNumber = "input_number"
    case inputOption = "input_option"

    case unknown
}

который отображает свойство строки JSON. Автоматическая сериализация и десериализация прекрасно работают, но я обнаружил, что если встречается другая строка, десериализация не выполняется.

Можно ли определить unknown случай, который отображает любой другой доступный случай?

Это может быть очень полезно, так как эти данные поступают из RESTFul API, который, возможно, может измениться в будущем.

Ответы

Ответ 1

Вы можете расширить тип Codable и назначить значение по умолчанию в случае сбоя:

enum Type: String {
    case text,
         image,
         document,
         profile,
         sign,
         inputDate = "input_date",
         inputText = "input_text" ,
         inputNumber = "input_number",
         inputOption = "input_option",
         unknown
}
extension Type: Codable {
    public init(from decoder: Decoder) throws {
        self = try Type(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
    }
}

редактировать/обновление:

В Swift 5 или более поздней версии мы можем создать протокол, который по умолчанию будет соответствовать последнему случаю перечисления строк CaseIterable:

protocol CaseIterableDefaultsLast: Codable & CaseIterable & RawRepresentable
    where Self.RawValue == String, Self.AllCases: BidirectionalCollection { }

extension CaseIterableDefaultsLast {
    init(from decoder: Decoder) throws {
        self = try Self(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? Self.allCases.last!
    }
}

Тестирование игровой площадки:

enum Type: String, CaseIterableDefaultsLast {
    case text, image, document, profile, sign, inputDate = "input_date", inputText = "input_text" , inputNumber = "input_number", inputOption = "input_option", unknown
}

let types = try! JSONDecoder().decode([Type].self , from: Data(#"["text","image","sound"]"#.utf8))  // [text, image, unknown]

Ответ 2

Вы можете удалить исходный тип для своего Type и сделать неизвестный случай, который обрабатывает связанное значение. Но это стоит дорого. Вам как-то нужны исходные значения для ваших дел. Вдохновленный от этого и этих ответов SO, я придумал это элегантное решение вашей проблемы.

Чтобы иметь возможность хранить необработанные значения, мы будем поддерживать другое перечисление, но как личное:

enum Type {
    case text
    case image
    case document
    case profile
    case sign
    case inputDate
    case inputText
    case inputNumber
    case inputOption
    case unknown(String)

    // Make this private
    private enum RawValues: String, Codable {
        case text = "text"
        case image = "image"
        case document = "document"
        case profile = "profile"
        case sign = "sign"
        case inputDate = "input_date"
        case inputText = "input_text"
        case inputNumber = "input_number"
        case inputOption = "input_option"
        // No such case here for the unknowns
    }
}

Переместите часть encoding и decoding в расширения:

Декодируемая часть:

extension Type: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        // As you already know your RawValues is String actually, you decode String here
        let stringForRawValues = try container.decode(String.self) 
        // This is the trick here...
        switch stringForRawValues { 
        // Now You can switch over this String with cases from RawValues since it is String
        case RawValues.text.rawValue:
            self = .text
        case RawValues.image.rawValue:
            self = .image
        case RawValues.document.rawValue:
            self = .document
        case RawValues.profile.rawValue:
            self = .profile
        case RawValues.sign.rawValue:
            self = .sign
        case RawValues.inputDate.rawValue:
            self = .inputDate
        case RawValues.inputText.rawValue:
            self = .inputText
        case RawValues.inputNumber.rawValue:
            self = .inputNumber
        case RawValues.inputOption.rawValue:
            self = .inputOption

        // Now handle all unknown types. You just pass the String to Type unknown case. 
        // And this is true for every other unknowns that aren't defined in your RawValues
        default: 
            self = .unknown(stringForRawValues)
        }
    }
}

Кодируемая часть:

extension Type: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .text:
            try container.encode(RawValues.text)
        case .image:
            try container.encode(RawValues.image)
        case .document:
            try container.encode(RawValues.document)
        case .profile:
            try container.encode(RawValues.profile)
        case .sign:
            try container.encode(RawValues.sign)
        case .inputDate:
            try container.encode(RawValues.inputDate)
        case .inputText:
            try container.encode(RawValues.inputText)
        case .inputNumber:
            try container.encode(RawValues.inputNumber)
        case .inputOption:
            try container.encode(RawValues.inputOption)

        case .unknown(let string): 
            // You get the actual String here from the associated value and just encode it
            try container.encode(string)
        }
    }
}

Примеры:

Я просто обернул его в структуру контейнера (потому что мы будем использовать JSONEncoder/JSONDecoder) как:

struct Root: Codable {
    let type: Type
}

Для значений, отличных от неизвестного случая:

let rootObject = Root(type: Type.document)
do {
    let encodedRoot = try JSONEncoder().encode(rootObject)
    do {
        let decodedRoot = try JSONDecoder().decode(Root.self, from: encodedRoot)
        print(decodedRoot.type) // document
    } catch {
        print(error)
    }
} catch {
    print(error)
}

Для значений с неизвестным случаем:

let rootObject = Root(type: Type.unknown("new type"))
do {
    let encodedRoot = try JSONEncoder().encode(rootObject)
    do {
        let decodedRoot = try JSONDecoder().decode(Root.self, from: encodedRoot)
        print(decodedRoot.type) // unknown("new type")
    } catch {
        print(error)
    }
} catch {
    print(error)
}

Я приведу пример с локальными объектами. Вы можете попробовать с ответом REST API.

Ответ 3

Здесь вариант, основанный на ответе nayem, который предлагает немного более оптимизированный синтаксис, используя необязательную привязку внутренней инициализации RawValues:

enum MyEnum: Codable {

    case a, b, c
    case other(name: String)

    private enum RawValue: String, Codable {

        case a = "a"
        case b = "b"
        case c = "c"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let decodedString = try container.decode(String.self)

        if let value = RawValue(rawValue: decodedString) {
            switch value {
            case .a:
                self = .a
            case .b:
                self = .b
            case .c:
                self = .c
            }
        } else {
            self = .other(name: decodedString)
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()

        switch self {
        case .a:
            try container.encode(RawValue.a)
        case .b:
            try container.encode(RawValue.b)
        case .c:
            try container.encode(RawValue.c)
        case .other(let name):
            try container.encode(name)
        }
    }
}

Если вы уверены, что все существующие имена имен enum соответствуют строковым значениям, которые они представляют, вы можете оптимизировать RawValue для:

private enum RawValue: String, Codable {

    case a, b, c
}

... и encode(to:) to:

func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()

    if let rawValue = RawValue(rawValue: String(describing: self)) {
        try container.encode(rawValue)
    } else if case .other(let name) = self {
        try container.encode(name)
    }
}

Здесь практический пример использования этого, например, вы хотите моделировать SomeValue, у которого есть свойство, которое вы хотите моделировать как перечисление:

struct SomeValue: Codable {

    enum MyEnum: Codable {

        case a, b, c
        case other(name: String)

        private enum RawValue: String, Codable {

            case a = "a"
            case b = "b"
            case c = "letter_c"
        }

        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            let decodedString = try container.decode(String.self)

            if let value = RawValue(rawValue: decodedString) {
                switch value {
                case .a:
                    self = .a
                case .b:
                    self = .b
                case .c:
                    self = .c
                }
            } else {
                self = .other(name: decodedString)
            }
        }

        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()

            switch self {
            case .a:
                try container.encode(RawValue.a)
            case .b:
                try container.encode(RawValue.b)
            case .c:
                try container.encode(RawValue.c)
            case .other(let name):
                try container.encode(name)
            }
        }
    }

}

let jsonData = """
[
    { "value": "a" },
    { "value": "letter_c" },
    { "value": "c" },
    { "value": "Other value" }
]
""".data(using: .utf8)!

let decoder = JSONDecoder()

if let values = try? decoder.decode([SomeValue].self, from: jsonData) {
    values.forEach { print($0.value) }

    let encoder = JSONEncoder()

    if let encodedJson = try? encoder.encode(values) {
        print(String(data: encodedJson, encoding: .utf8)!)
    }
}


/* Prints:
 a
 c
 other(name: "c")
 other(name: "Other value")
 [{"value":"a"},{"value":"letter_c"},{"value":"c"},{"value":"Other value"}]
 */

Ответ 4

Вы должны реализовать init(from decoder: Decoder) throws инициализатор и проверяет допустимое значение:

struct SomeStruct: Codable {

    enum SomeType: String, Codable {
        case text
        case image
        case document
        case profile
        case sign
        case inputDate = "input_date"
        case inputText = "input_text"
        case inputNumber = "input_number"
        case inputOption = "input_option"

        case unknown
    }

    var someType: SomeType

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        someType = (try? values.decode(SomeType.self, forKey: .someType)) ?? .unknown
    }

}

Ответ 5

Добавьте это расширение и установите YourEnumName.

extension <#YourEnumName#>: Codable {
    public init(from decoder: Decoder) throws {
        self = try <#YourEnumName#>(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
    }
}

Ответ 6

@LeoDabus спасибо за ваши ответы. Я немного изменил их, чтобы сделать протокол для перечислений String, который, кажется, работает для меня:

protocol CodableWithUnknown: Codable {}
extension CodableWithUnknown where Self: RawRepresentable, Self.RawValue == String {
    init(from decoder: Decoder) throws {
        do {
            try self = Self(rawValue: decoder.singleValueContainer().decode(RawValue.self))!
        } catch {
            if let unknown = Self(rawValue: "unknown") {
                self = unknown
            } else {
                throw error
            }
        }
    }
}

Ответ 7

enum Type: String, Codable, Equatable {
    case image
    case document
    case unknown

    public init(from decoder: Decoder) throws {
        guard let rawValue = try? decoder.singleValueContainer().decode(String.self) else {
            self = .unknown
            return
        }
        self = Type(rawValue: rawValue) ?? .unknown
    }
}