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



我正在使用Codable作为我的WebRequest响应,该响应返回一些预定义的字符串或数字。所以,我正在使用枚举。但是当一些意想不到的值到达响应时,我的 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 nil 然后是 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 的响应中不会出现一个密钥。

我建议编写一个属性包装器来为您处理此问题。

具体来说,让我们编写一个名为NilOnDecodingError的属性包装器,它将任何DecodingError转换为nil

以下是NilOnDecodingError声明:

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

我们已经将其定义为包装任何类型,存储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类型EncodableEncodable它:

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

现在我们可以包装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))

您需要在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
}
}

在我看来,编译器为枚举类型选择了错误的 init,而不是init(rawValue)它使用init(from:)进行解码(这在某种程度上是有道理的)

这是一个解决方案,我们通过使用WebUser中的自定义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
}

然后它应该开箱即用,您无需编写任何自定义代码。

我喜欢@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)
}

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

最新更新