现实使用 Codable 上遇到的 Decode 问题场景总汇(下)
合理的处理 Response Null 栏位资料、不一定都要重写 init decoder

Photo by Zan
前言
既上篇「 现实使用 Codable 上遇到的 Decode 问题场景总汇 」后,开发进度继续迈进又遇到了新的场景新的问题,故出了此下篇,继续把遇到的情景、研究心路都记录下来,方便日后回头查阅。
前篇主要解决了 JSON String -> Entity Object 的 Decodable Mapping,有了 Entity Object 后我们可以转换成 Model Object 在程式内传递使用、View Model Object 处理资料显示逻辑…等等; 另一方面我们需要将 Entity 转换成 NSManagedObject 存入本地 Core Data 中 。
主要问题
假设我们的歌曲 Entity 结构如下:
struct Song: Decodable {
var id: Int
var name: String?
var file: String?
var converImage: String?
var likeCount: Int?
var like: Bool?
var length: Int?
}
因 API EndPoint 并不一定会回传完整资料栏位(只有 id 是一定会给),所以除 id 之外的栏位都是 Optional;例如:取得歌曲资讯的时候会回传完整结构,但若是对歌曲收藏喜欢时仅会回传 id 、 likeCount 、 like 三个有关联更动的栏位资料。
我们希望 API Response 有什么栏位资料都能一并存入 Core Data 里,如果资料已存在就更新变动的栏位资料(incremental update)。
但此时问题就出现了:Codable Decode 换成 Entity Object 后我们无法区别 「资料栏位是想要设成 nil」 还是 「Response 没给」
A Response:
{
"id": 1,
"file": null
}
对于 A Response、B Response 的 file 来说都是 null 、但意义不一一样 ;A 是想把 file 栏位设为 null (清空原本资料)、 B 是想 update 其他资料,单纯没给 file 栏位而已。
Swift 社群有开发者提出 增加类似 date Strategy 的 null Strategy 在 JSONDecoder 中 ,让我们能区分以上状况,但目前没有计划要加入。
解决方案
如前所述,我们的架构是JSON String -> Entity Object -> NSManagedObject,所以当拿到 Entity Object 时已经是 Decode 后的结果了,没有 raw data 可以操作;这边当然可以拿原始 JSON String 比对操作,但与其这样不如不要用 Codable。
首先参考 上一篇 使用 Associated Value Enum 当容器装值。
enum OptionalValue<T: Decodable>: Decodable {
case null
case value(T)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let value = try? container.decode(T.self) {
self = .value(value)
} else {
self = .null
}
}
}
使用泛型,T 为真实资料栏位型别;.value(T) 能放 Decode 出来的值、.null 则代表值是 null。
struct Song: Decodable {
enum CodingKeys: String, CodingKey {
case id
case file
}
var id: Int
var file: OptionalValue<String>?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
if container.contains(.file) {
self.file = try container.decode(OptionalValue<String>.self, forKey: .file)
} else {
self.file = nil
}
}
}
var jsonData = """
{
"id":1
}
""".data(using: .utf8)!
var result = try! JSONDecoder().decode(Song.self, from: jsonData)
print(result)
jsonData = """
{
"id":1,
"file":null
}
""".data(using: .utf8)!
result = try! JSONDecoder().decode(Song.self, from: jsonData)
print(result)
jsonData = """
{
"id":1,
"file":\"https://test.com/m.mp3\"
}
""".data(using: .utf8)!
result = try! JSONDecoder().decode(Song.self, from: jsonData)
print(result)
范例先简化成只有
id、file两个资料栏位。
Song Entity 自行复写实践 Decode 方式,使用 contains(.KEY) 方法判断 Response 有无给该栏位(无论值是什么),如果有就 Decode 成 OptionalVale ;OptionalValue Enum 中会再对真正我们要的值做 Decode ,如果有值 Decode 成功则会放在 .value(T) 、如果给的值是 null (或 decode 失败)则放在 .null 。
-
Response 有给栏位&值时:OptionalValue.value(VALUE)
-
Response 有给栏位&值是 null 时:OptionalValue.null
-
Response 没给栏位时:nil
这样就能区分出是有给栏位还是没给栏位,后续要写入 Core Data 时就能判断是要更新栏位成 null、还是没有要更新此栏位。
其他研究 — Double Optional ❌
Optional!Optional! 在 Swift 上就很适合处理这个场景。
struct Song: Decodable {
var id: Int
var name: String??
var file: String??
var converImage: String??
var likeCount: Int??
var like: Bool??
var length: Int??
}
-
Response 有给栏位&值时:Optional(VALUE)
-
Response 有给栏位&值是 null 时:Optional(nil)
-
Response 没给栏位时:nil
但是….Codable JSONDecoder Decode 对 Double Optional 跟 Optional 都是 decodeIfPresent 在处理,都视为 Optional ,不会特别处理 Double Optional;所以结果跟原本一样。
其他研究 — Property Wrapper ❌
本来预想可以用 Property Wrapper 做优雅的封装,例如:
@OptionalValue var file: String?
但还没开始研究细节就发现有 Property Wrapper 标记的 Codable Property 栏位,API Response 就必须要有该栏位,否则会出现 keyNotFound error,即使该栏位是 Optional。?????
官方论坛也有针对此问题的 讨论串 …估计之后会修正。
所以选用 BetterCodable 、 CodableWrappers 这类套件的时候要考虑到目前 Property Wrapper 的这个问题。
其他问题场景
1.API Response 使用 0/1 代表 Bool,该如何 Decode?
import Foundation
struct Song: Decodable {
enum CodingKeys: String, CodingKey {
case id
case name
case like
}
var id: Int
var name: String?
var like: Bool?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.name = try container.decodeIfPresent(String.self, forKey: .name)
if let intValue = try container.decodeIfPresent(Int.self, forKey: .like) {
self.like = (intValue == 1) ? true : false
} else if let boolValue = try container.decodeIfPresent(Bool.self, forKey: .like) {
self.like = boolValue
}
}
}
var jsonData = """
{
"id": 1,
"name": "告五人",
"like": 0
}
""".data(using: .utf8)!
var result = try! JSONDecoder().decode(Song.self, from: jsonData)
print(result)
延伸前篇,我们可以自己在 init Decode 中,Decode 成 int/Bool 然后自己赋值、这样就能扩充原本的栏位能接受 0/1/true/false了。
2.不想要每每都要重写 init decoder
在不想要自干 Decoder 的情况下,复写原本的 JSON Decoder 扩充更多功能。
我们可以自行 extenstion KeyedDecodingContainer 对 public 方法自行定义,swift 会优先执行 module 下我们重定义的方法,复写掉原本 Foundation 的实作。
影响的就是整个 module。
且不是真的 override,无法 call super.decode,也要小心不要自己 call 自己(EX: decode(Bool.Type,for:key) in decode(Bool.Type,for:key) )
decode 有两个方法:
-
decode(Type, forKey:) 处理非 Optional 资料栏位
-
decodeIfPresent(Type, forKey:) 处理 Optional 资料栏位
范例1. 前述的主要问题就我们可以直接 extenstion:
extension KeyedDecodingContainer {
public func decodeIfPresent<T>(_ type: T.Type, forKey key: Self.Key) throws -> T? where T : Decodable {
//better:
switch type {
case is OptionalValue<String>.Type,
is OptionalValue<Int>.Type:
return try? decode(type, forKey: key)
default:
return nil
}
// or just return try? decode(type, forKey: key)
}
}
struct Song: Decodable {
var id: Int
var file: OptionalValue<String>?
}
因主要问题是 Optional 资料栏位、Decodable 类型,所以我们复写的是 decodeIfPresent<T: Decodable> 这个方法。
这边推测原本 decodeIfPresent 的实作是,如果资料是 null 或 Response 未给 会直接 return nil,并不会真的跑 decode。
所以原理也很简单,只要 Decodable Type 是 OptionValue
范例2. 问题场景1 也能用此方法扩充:
extension KeyedDecodingContainer {
public func decodeIfPresent(_ type: Bool.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Bool? {
if let intValue = try? decodeIfPresent(Int.self, forKey: key) {
return (intValue == 1) ? (true) : (false)
} else if let boolValue = try? decodeIfPresent(Bool.self, forKey: key) {
return boolValue
}
return nil
}
}
struct Song: Decodable {
enum CodingKeys: String, CodingKey {
case id
case name
case like
}
var id: Int
var name: String?
var like: Bool?
}
var jsonData = """
{
"id": 1,
"name": "告五人",
"like": 1
}
""".data(using: .utf8)!
var result = try! JSONDecoder().decode(Song.self, from: jsonData)
print(result)
结语
Codable 在使用上的各种奇技淫巧都用的差不多了,有些其实很绕,因为 Codable 的约束性实在太强、牺牲许多现实开发上需要的弹性;做到最后甚至开始思考为何当初要选择 Codable,优点越做越少….



留言 · Comments