当Swift iOS中出现意外值时,如何将nil设置为可编码枚举

vkc1a9a2  于 2023-02-07  发布在  Swift
关注(0)|答案(5)|浏览(114)

我使用Codable作为WebRequest响应,它返回一些预定义的字符串或数字。因此,我使用Enums作为这些响应。但是当一些意外的值到达Response时,我的Codable无法解码。
下面是一些代码,以便更好地理解.

class WebUser: Codable, Equatable {
    static func == (lhs: WebUser, rhs: WebUser) -> Bool {
        return lhs.id == rhs.id
    }
    ...
    var mobileNumberPrivacy: CommonPrivacyOption?
    var emailPrivacy: CommonPrivacyOption?
    var dobPrivacy: CommonPrivacyOption?
    ...
}

enum CommonPrivacyOption: Int, CaseIterable, Codable {
    case privacyOnlyMe = 1, privacyPublic, privacyFriends, privacyFriendsOfFriends

    //Does not help this optional init function
    /*init?(rawValue: Int) {
        switch rawValue {
            case 1: self = .privacyOnlyMe
            case 2: self = .privacyPublic
            case 3: self = .privacyFriends
            case 4: self = .privacyFriendsOfFriends
            default: return nil
        }
    }*/

}

但是有时候我会从WebServer得到,0表示dobPrivacy,此时我会得到DecodingError.dataCorrupted异常,上下文为Cannot initialize CommonPrivacyOption from invalid Int value 0
当我得到其他值时,我期望dobPrivacy为零,然后是1/2/3/4。

    • 编辑:**
let dict1 = [
    "id": 2,
    "mobileNumberPrivacy": 3,
    "emailPrivacy": 4,
    "dobPrivacy": 0 // Works perfectly with 1
]
do {
    let data1 = try JSONSerialization.data(withJSONObject: dict1, options: .prettyPrinted)
    let user1 = try JSONDecoder().decode(WebUser.self, from: data1)
    print("User1 created")
}
catch DecodingError.dataCorrupted(let context) {
    print(context.codingPath)
    print(context.debugDescription)
}
catch {
    print(error.localizedDescription)
}

我使用这个相同的可编码WebUser对象的个人资料细节,搜索用户和更多。所以可能是一些时候一个以上的关键字不会出现在WebRequest的响应。

of1yzvn4

of1yzvn41#

我建议编写一个属性 Package 器来为您处理这个问题。
具体来说,让我们编写一个名为NilOnDecodingError的属性 Package 器,它将任何DecodingError转换为nil
下面是NilOnDecodingError的声明:

@propertyWrapper
public struct NilOnDecodingError<Wrapped> {
    public init(wrappedValue: Wrapped?) {
        self.wrappedValue = wrappedValue
    }

    public var wrappedValue: Wrapped?
}

我们定义它来 Package 任何类型,存储一个Optional
现在,当Wrapped类型为Decodable时,我们可以使其符合Decodable

extension NilOnDecodingError: Decodable where Wrapped: Decodable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            wrappedValue = .some(try container.decode(Wrapped.self))
        } catch is DecodingError {
            wrappedValue = nil
        }
    }
}

Wrapped类型是Encodable时,我们可能也希望它是Encodable

extension NilOnDecodingError: Encodable where Wrapped: Encodable {
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        if let value = wrappedValue {
            try container.encode(value)
        } else {
            try container.encodeNil()
        }
    }
}

现在我们可以 Package WebUser的相应字段:

class WebUser: Codable {
    let id: String

    @NilOnDecodingError
    var mobileNumberPrivacy: CommonPrivacyOption?

    @NilOnDecodingError
    var emailPrivacy: CommonPrivacyOption?

    @NilOnDecodingError
    var dobPrivacy: CommonPrivacyOption?
}

为了进行测试,我们需要打印解码用户的字段:

extension WebUser: CustomStringConvertible {
    var description: String {
        return """
        WebUser(
            id: \(id),
            mobileNumberPrivacy: \(mobileNumberPrivacy.map { "\($0)" } ?? "nil"),
            emailPrivacy: \(emailPrivacy.map { "\($0)" } ?? "nil")),
            dobPrivacy: \(dobPrivacy.map { "\($0)" } ?? "nil")))
        """
    }
}

现在我们可以尝试一下:

let json = """
{
    "id": "mrugesh",
    "mobileNumberPrivacy": 1,
    "emailPrivacy": 2,
    "dobPrivacy": 1000
}
"""

let user = try! JSONDecoder().decode(WebUser.self, from: json.data(using: .utf8)!)
print(user)

输出:

WebUser(
    id: mrugesh,
    mobileNumberPrivacy: privacyOnlyMe,
    emailPrivacy: privacyPublic),
    dobPrivacy: nil))
zengzsys

zengzsys2#

您需要在WebUser中创建一个自定义的Decodable初始化器:

class WebUser: Codable {
    var dobPrivacy: CommonPrivacyOption?
    // Rest of your properties here.

    enum CodingKeys: String, CodingKey {
        case dobPrivacy
        // Add a case for each property you want to decode here.
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        // Use optional try to decode your enum so that when the
        // decode fails because of wrong Int value, it will assign nil.
        dobPrivacy = try? container.decode(CommonPrivacyOption.self, forKey: .dobPrivacy)
    }
}

或者,您可以在CommonPrivacyOption中实现Decodable初始化器,并添加一个额外的case unknown,如下所示:

enum CommonPrivacyOption: Int, Codable {
    case privacyOnlyMe = 1
    case privacyPublic, privacyFriends, privacyFriendsOfFriends
    case unknown

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

        // Try to initialize Self from value, if 
        // value is not 1, 2, 3, or 4, initialize Self to 
        // the unknown case.
        self = .init(rawValue: value) ?? .unknown
    }
}
5t7ly7z5

5t7ly7z53#

在我看来,编译器为枚举类型选择了错误的init,而不是init(rawValue),它使用init(from:)进行解码(这在某种程度上是有意义的)
下面是一个解决方案,我们使用WebUser中的自定义init(from)来覆盖此行为,该自定义init(from)对原始值进行解码,然后创建一个枚举项

required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    if let value = try container.decodeIfPresent(CommonPrivacyOption.RawValue.self, forKey: .mobileNumberPrivacy), let mobileNumberPrivacy = CommonPrivacyOption(rawValue: value) {
        self.mobileNumberPrivacy = mobileNumberPrivacy
    }
    if let value = try container.decodeIfPresent(CommonPrivacyOption.RawValue.self, forKey: .emailPrivacy), let emailPrivacy = CommonPrivacyOption(rawValue: value) {
        self.emailPrivacy = emailPrivacy
    }
    if let value = try container.decodeIfPresent(CommonPrivacyOption.RawValue.self, forKey: .dobPrivacy), let dobPrivacy = CommonPrivacyOption(rawValue: value) {
        self.dobPrivacy = dobPrivacy
    }
}

下面是一个小示例

extension WebUser: CustomStringConvertible {
    var description: String {
        "Mobile: \(mobileNumberPrivacy?.rawValue), email: \(emailPrivacy?.rawValue), dob: \(dobPrivacy?.rawValue)"
    }
}

let data = """
{
    "mobileNumberPrivacy": 1,
    "dobPrivacy": 0
}
""".data(using: .utf8)!

do {
    let result = try JSONDecoder().decode(WebUser.self, from: data)
    print(result)
} catch {
    print(error)
}

移动的:可选(1),电子邮件:无,出生日期:无
当然,如果您可以改变主意,将0转换为nil,那么我建议您扩展枚举以支持0值

enum CommonPrivacyOption: Int, CaseIterable, Codable {
    case none = 0
    case privacyOnlyMe = 1, privacyPublic, privacyFriends, privacyFriendsOfFriends
}

那么它应该是开箱即用的,您不需要编写任何自定义代码。

vcudknz3

vcudknz34#

我喜欢@alobaili的回答,因为它简单,提供了一个很好的解决方案。我想改进的一件事是使它更通用,以便任何Decodable(和Codable)都可以用更少的代码来完成它。

extension Decodable where Self: RawRepresentable, RawValue: Decodable {
    
    static func initializedOptionalWith(decoder: Decoder, defaultValue: Self) throws -> Self {
        
        let container = try decoder.singleValueContainer()
        let value = try container.decode(RawValue.self)
        
        return .init(rawValue: value) ?? defaultValue
    }
}

用法:

init(from decoder: Decoder) throws {
    self = try .initializedOptionalWith(decoder: decoder, defaultValue: .unknown)
}
nvbavucw

nvbavucw5#

谢谢Rob Mayoff的精彩回答。我只想补充一点-如果属性丢失,即使属性是可选的,解码也会失败。为了防止这种失败,请添加以下扩展:

extension KeyedDecodingContainer {
    func decode<T>(_ type: NilOnDecodingError<T>.Type, forKey key: Self.Key) throws -> NilOnDecodingError<T> where T: Decodable {
        try decodeIfPresent(type, forKey: key) ?? NilOnDecodingError<T>(wrappedValue: nil)
    }
}

相关问题