Enum 与 Decodable

上回提到 Enum with RawValue 的 Decodable 处理,最近发现了更方便的处理方式,这里总结一下。


第一版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public enum Category1: String, Decodable {

case left

case right
}

public struct Test1: Decodable {

public let category1: Category1

public let int: Int
}

let json11 = """
{
"category1": "left",
"int": 1
}
""".data(using: .utf8)!

let json12 = """
{
"category1": "east",
"int": 1
}
""".data(using: .utf8)!

do {
let decoder = JSONDecoder()
let model11 = try decoder.decode(Test1.self, from: json11)
print(model11)
// Test1(category1: Category1.left, int: 1)
} catch {
print(error)
}

do {
let decoder = JSONDecoder()
let model12 = try decoder.decode(Test1.self, from: json12)
print(model12)
} catch {
print(error)
// dataCorrupted(Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "category1", intValue: nil)], debugDescription: "Cannot initialize Category1 from invalid String value east", underlyingError: nil))
}

category1 返回一个 Category1 未定义值的时候,解析出错。

第二版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public struct Test2: Decodable {

public let category1: Category1?

public let int: Int
}

do {
let decoder = JSONDecoder()
let model22 = try decoder.decode(Test2.self, from: json12)
print(model22)
} catch {
print(error)
// dataCorrupted(Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "category1", intValue: nil)], debugDescription: "Cannot initialize Category1 from invalid String value east", underlyingError: nil))
}

即使 category1 声明为 Category1?,返回未定义值的时候,仍然解析出错。

第三版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public enum Category3: String, Decodable {

case left

case right

case unknown

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let rawValue = try container.decode(String.self)
self = Category3(rawValue: rawValue) ?? .unknown
}
}

public struct Test3: Decodable {

public let category1: Category3

public let int: Int
}

do {
let decoder = JSONDecoder()
let model32 = try decoder.decode(Test3.self, from: json12)
print(model32)
// Test3(category1: Category3.unknown, int: 1)
} catch {
print(error)
}

Category3 增加 unknown 成员并重写 init(from:) 方法,可以在返回未定义值的时候回退到 unknown,防止解析出错。

如果每一个 Enum with RawValue 都需要单独增加 unknown 成员和重写 init(from:) 方法,无疑是低效的。

我们都知道,Enum with RawValue 默认实现了 RawRepresentable 协议。我们可以从这里入手,通过强大的 extension 来避免模板代码。

第四版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public protocol UnknownCompatible: RawRepresentable {

static var unknown: Self { get }
}

extension UnknownCompatible where Self: Decodable, RawValue: Decodable {

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let rawValue = try container.decode(RawValue.self)
self = Self(rawValue: rawValue) ?? .unknown
}
}

public enum Category4: String, UnknownCompatible, Decodable {

case left

case right

case unknown
}

public struct Test4: Decodable {

public let category1: Category4

public let int: Int
}

do {
let decoder = JSONDecoder()
let model42 = try decoder.decode(Test4.self, from: json12)
print(model42)
// Test4(category1: Category4.unknown, int: 1)
} catch {
print(error)
}

声明一个继承 RawRepresentable 协议的 UnknownCompatible 协议,协议只约束了一个 unknown 静态计算属性。给 UnknownCompatible 增加 init(from") 的默认实现,要求是 SelfRawValue 实现 Decodable 协议。

Category4 中,只需要声明实现 UnknownCompatible 协议,即可免费获得上面的初始化方法。这里用到了 Swift 5.3 的 SE-0230 Enum cases as protocol witnesses 提案。

自定义

上面的问题,对应的是 DecodingError.dataCorrupted 错误。除此之外,还有 keyNotFound / typeMismatch / valueNotFound 错误。我们可以通过自定义来处理部分错误,并且回退到 unknown

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public protocol UnknownCompatible2: RawRepresentable {

static var unknown: Self { get }

static var allowDataCorrupted: Bool { get }

static var allowTypeMismatch: Bool { get }

static var allowValueNotFound: Bool { get }
}

extension UnknownCompatible2 {

public static var allowDataCorrupted: Bool { true }

public static var allowTypeMismatch: Bool { false }

public static var allowValueNotFound: Bool { true }
}

extension UnknownCompatible2 where Self: Decodable, RawValue: Decodable {

public init(from decoder: Decoder) throws {
do {
let container = try decoder.singleValueContainer()
let rawValue = try container.decode(RawValue.self)

if let value = Self(rawValue: rawValue) {
self = value
} else if Self.allowDataCorrupted {
self = .unknown
} else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Cannot initialize \(Self.self) from invalid \(RawValue.self) value \(rawValue)"
)
}
} catch DecodingError.typeMismatch where Self.allowTypeMismatch {
self = .unknown
} catch DecodingError.valueNotFound where Self.allowValueNotFound {
self = .unknown
} catch {
throw error
}
}
}

public enum Category5: String, UnknownCompatible2, Decodable {

case left

case right

case unknown

public static var allowTypeMismatch: Bool = true
}

public struct Test5: Decodable {

public let category1: Category5

public let category2: Category5

public let category3: Category5
}

let json2 = """
{
"category1": "east",
"category2": true,
"category3": null
}
""".data(using: .utf8)!

do {
let decoder = JSONDecoder()
let model5 = try decoder.decode(Test5.self, from: json2)
print(model5)
// Test5(category1: Category5.unknown, category2: Category5.unknown, category3: Category5.unknown)
} catch {
print(error)
}

除了 keyNotFound 错误,其余三个错误都可以在 UnknownCompatible2 协议的 init(from:) 默认实现中得到处理。这里用到了 Swift 5.3 的 SE-0276 Multi-Pattern Catch Clauses 提案。

总结

通过 Swift 强大的协议和扩展系统,我们可以避免书写重复的模板代码。但应该注意,统一实现需要处理好边界条件,不然一步错,步步错。

小尾巴

decode 参数直接放在枚举类型下面,点语法访问时有点别扭,可以考虑放在 DecodingOption 这样的结构体里,或者使用 Property Wrapper 来自定义 decode 行为。或许下一次可以写一下(先咕咕咕再说)。

参考