Codable enum with default case in Swift 4

痴心易碎 提交于 2019-12-18 13:59:11

问题


I have defined an enum as follows:

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
}

that maps a JSON string property. The automatic serialization and deserialization works fine, but I found that if a different string is encountered, the deserialization fails.

Is it possible to define an unknown case that maps any other available case?

This can be very useful, since this data comes from a RESTFul API that, maybe, can change in the future.


回答1:


You can extend your Codable Type and assign a default value in case of failure:

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

edit/update:

Xcode 11.2 • Swift 5.1 or later

Create a protocol that defaults to last case of a CaseIterable & Decodable enumeration:

protocol CaseIterableDefaultsLast: Decodable & CaseIterable & RawRepresentable
where Self.RawValue: Decodable, Self.AllCases: BidirectionalCollection { }

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

Playground testing:

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:


You can drop the raw type for your Type and make unknown case that handles associated value. But this comes at a cost. You somehow need the raw values for your cases. Inspired from this and this SO answers I came up with this elegant solution to your problem.

To be able to store the raw values, we will maintain another enum, but as private:

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

Move the encoding & decoding part to extensions:

Decodable part:

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's unknown case. 
        // And this is true for every other unknowns that aren't defined in your RawValues
        default: 
            self = .unknown(stringForRawValues)
        }
    }
}

Encodable part:

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

Examples:

I just wrapped it in a container structure(because we'll be using JSONEncoder/JSONDecoder) as:

struct Root: Codable {
    let type: Type
}

For values other than unknown case:

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

For values with unknown case:

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

I put the example with local objects. You can try with your REST API response.




回答3:


Here's an alternative based on nayem's answer that offers a slightly more streamlined syntax by using optional binding of the inner RawValues initialization:

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

If you are certain that all your existing enum case names match the underlying string values they represent, you could streamline RawValue to:

private enum RawValue: String, Codable {

    case a, b, c
}

...and 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)
    }
}

Here's a practical example of using this, e.g., you want to model SomeValue that has a property you want to model as an enum:

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:


You have to implement the init(from decoder: Decoder) throws initializer and check for a valid value:

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:


Add this extension and set YourEnumName .

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



回答6:


@LeoDabus thanks for your answers. I modified them a bit to make a protocol for String enums that seems to work for me:

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


来源:https://stackoverflow.com/questions/49695780/codable-enum-with-default-case-in-swift-4

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!