有關Swift Codable解析成Dictionary的一些事

語言: CN / TW / HK

theme: cyanosis

前言

假設有一json: json {      "int": 1234,      "string": "測試",      "double": 10.0002232,      "ext": {          "intExt": 456,          "stringExt": "擴充套件",          "doubleExt": 456.001      } } 日常開發中,常見的會將資料實體確定好,然後再解析:

```swift struct TestCodable: Codable {     let int: Int     let string: String     let double: Double     let ext: Ext }

struct Ext: Codable {     let intExt: Int     let stringExt: String     let doubleExt: Double } 但正如json串中命名的`ext`,這是個擴充套件屬性,意味著需求應該是會在該欄位中**存在一些動態欄位**。這時,其實我們更希望將它解析成一個`Dictionary<String, Any>`字典型別swift struct TestCodable: Codable {     let int: Int     let string: String     let double: Double     let ext: [String: Any] } `` 遺憾的是,由於Dictionary型別在Codable是**沒有預設解析的邏輯**,或者說Any這個不確定型別**無法直接用Codable解析**,所以上述的程式碼其實是會報錯的。但這種做法,對於一些擁有靈活配置的需求來說,又是非常的合理。因此,本文就重點結合這一合理的需求**聊聊如何採用Codable庫來解析Dictionary,還有在研究過程中發現它與GRDB`的不相容性以及處理方案**。

Codable解析

使用

```swift let json = """ {      "int": 1234,      "string": "測試",      "double": 10.0002232 } """ // json -> 實體 let jsonData = json.data(using: .utf8)! let jsonDecoder = JSONDecoder.init() let a = try! jsonDecoder.decode(A.self, from: jsonData)

// 實體 -> json let jsonEncoder = JSONEncoder.init() let jsonData2 = try! jsonEncoder.encode(testCodable) let json2 = String.init(data: jsonData2, encoding: .utf8)! `` 上述程式碼是利用Codable提供的JSONDecoder將json串解析成資料實體和利用JSONEncoder`將資料實體打包成json串的過程。

```swift struct A: Codable { let int: Int let string: String let double: Double

enum Key: String, CodingKey {
    case int
    case string
    case double
}

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: Key.self)
    try container.encode(int, forKey: .int)
    try container.encode(string, forKey: .string)
    try container.encode(double, forKey: .double)
}

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: Key.self)
    self.int = try container.decode(Int.self, forKey: .int)
    self.string = try container.decode(String.self, forKey: .string)
    self.double = try container.decode(Double.self, forKey: .double)
}

} ``Codableencodedecode都會通過實體的func encode(to encoder: Encoder)init(from decoder: Decoder)`,如果開發者自己不自定義的話,編譯器會自動生成

為什麼不能解析成Any型別

回到開頭說的例子,假如我們定義成Dictionary<String, Any>的話會報以下錯誤 swift struct TestCodable: Codable {     let int: Int     let string: String     let double: Double // Error: Type 'TestCodable' does not conform to protocol 'Decodable' // Error: Type 'TestCodable' does not conform to protocol 'Encodable'     let ext: [String: Any] } 字面理解就是,由於[String: Any]的定義,導致Codable無法自動生成編解碼的定義,也就是說Codable只能支援自身定義好的解析,如整型或者是繼承了Codable的型別,而Any是一個例外。 ```swift public mutating func encode(_ value: Int, forKey key: KeyedEncodingContainer.Key) throws

public mutating func encode(_ value: T, forKey key: KeyedEncodingContainer.Key) throws where T : Encodable

public func decode(_ type: Int.Type, forKey key: KeyedDecodingContainer.Key) throws -> Int

public func decode(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) throws -> T where T : Decodable ```

所以解決問題的核心就是需要一個支援Dictionary<String, Any>encodedecode方法

原始碼分析

JSONEncoder、JSONDecoder原始碼:

JSONEncoder.swift

CodingKey

當我們呼叫JSONDecoder#decode方法時,內部其實是利用JSONSerialization將Json轉換為字典的。 ```swift // JSONDecoder open func decode(_ type: T.Type, from data: Data) throws -> T { let topLevel: Any do { topLevel = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) } catch { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error)) }

let decoder = __JSONDecoder(referencing: topLevel, options: self.options)
guard let value = try decoder.unbox(topLevel, as: type) else {
    throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
}

return value

} 以`KeyedDecodingContainer`為例,字典最終會**儲存在`_JSONKeyedDecodingContainer#container`當中**。swift private struct _JSONKeyedDecodingContainer : KeyedDecodingContainerProtocol { typealias Key = K

// MARK: Properties
/// A reference to the decoder we're reading from.
private let decoder: __JSONDecoder

/// A reference to the container we're reading from.
private let container: [String : Any]

/// The path of coding keys taken to get to this point in decoding.
private(set) public var codingPath: [CodingKey]

所以理論上,**Json當中的所有欄位,都可以從`container`中取出**。而在`decode`方法中,這裡以Bool型解析為例:swift public func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key (_errorDescription(of: key)).")) }

self.decoder.codingPath.append(key)
defer { self.decoder.codingPath.removeLast() }

guard let value = try self.decoder.unbox(entry, as: Bool.self) else {
    throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead."))
}

return value

} `decode`方法中,會**通過`Key#stringValue`作為`key`取出`container`內對應的`value`**,而這裡的`Key`即為`CodingKey`型別,結合開頭的例子**可以定義為一個繼承自`CodingKey`的型別**,這裡可以是一個`enum`型別。swift public protocol CodingKey : CustomDebugStringConvertible, CustomStringConvertible, Sendable { var stringValue: String { get } init?(stringValue: String)

var intValue: Int? { get }
init?(intValue: Int)

}

enum Key: String, CodingKey { case int case string case double case ext } 這樣做的好處是,**明確知道Json中有哪些欄位,可以根據特定的key解析出value**,繼而生成實體。但**侷限性就是隻能解析明確了的key值**,明顯不符合本文需求。所以可以稍微做一下變形,實現一個繼承自`CodingKey`的普通型別:swift struct JSONCodingKeys: CodingKey { var stringValue: String

init(stringValue: String) {
    self.stringValue = stringValue
}

var intValue: Int?

init?(intValue: Int) {
    self.init(stringValue: "\(intValue)")
    self.intValue = intValue
}

} `` 這樣就可以將_JSONKeyedDecodingContainer#container中的key`都表示出來。

獲取字典中所有的key

swift // _JSONKeyedDecodingContainer public var allKeys: [Key] { return self.container.keys.compactMap { Key(stringValue: $0) } } allKeys可以從_JSONKeyedDecodingContainer物件中獲取到內部通過Json解析到的字典裡的key值,前提是Key型別要支援。意思是 - 假如我們定義的Key是一個enum型別,那麼返回的集合就只能包含在enum中定義的值,如:Key.intKey.string等。 - 假如我們定義的Key是一個正常的structclass,那就意味著字典中的全部key都會轉換成這個型別的物件,如:JSONCodingKeys.init("int")JSONCodingKeys.init("string")等。

解析[String: Any]型別字典

有了上述的CodingKeys的分析,就有了能解析出Dictionary<String, Any>的希望。先總結一下目前已有的基礎: 1. Codable不是完全不能解析Dictionary<String, Any>型別,而是需要我們手動重寫encodedecode方法。 2. 欄位名是能夠通過實現JSONCodingKeys例項一個物件來表示動態key的,譬如JSONCodingKeys.init("int")。 3. 結合2中的JSONCodingKeys,在_JSONKeyedDecodingContainer#allKeys中是可以獲取到當前這一級字典的所有key的

眾所周知,Json解析可以理解為一個從外向內遞迴的過程,所以結合上述的3點,就能夠在獲取到所有Key的前提下,逐層遞迴來生成一個Dictionary<String, Any>,但Codable沒有提供這個api,需要我們自己動手。

還是用開頭的例子 json {      "int": 1234,      "string": "測試",      "double": 10.0002232,      "ext": {          "intExt": 456,          "stringExt": "擴充套件",          "doubleExt": 456.001      } } 在解析到ext時,我們就可以通過JSONCodingKeys獲取所有key,使用到的是_JSONKeyedDecodingContainer#nestedContainer方法。 swift func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> { let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key) return try container.decode(type) } 通過_JSONKeyedDecodingContainer#nestedContainer獲取到的container,內部就會擁有ext那一級下的所有key值,如intExtstringExtdoubleExt。接著就可以逐個欄位嘗試來確定value的型別,最終遞迴生成一個 Dictionary<String, Any> ```swift func decode(_ type: Dictionary.Type) throws -> Dictionary { var dictionary = Dictionary()

for key in allKeys {
    if let boolValue = try? decode(Bool.self, forKey: key) {
        dictionary[key.stringValue] = boolValue
    } else if let stringValue = try? decode(String.self, forKey: key) {
        dictionary[key.stringValue] = stringValue
    } else if let intValue = try? decode(Int.self, forKey: key) {
        dictionary[key.stringValue] = intValue
    } else if let doubleValue = try? decode(Double.self, forKey: key) {
        dictionary[key.stringValue] = doubleValue
    } else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
        dictionary[key.stringValue] = nestedDictionary
    } else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
        dictionary[key.stringValue] = nestedArray
    }
}
return dictionary

} `encode`方法同理,文章後面會貼出詳細的程式碼。最後只需要**重寫資料實體的`encode`和`decode`方法**swift /* { "int": 1234, "string": "測試", "double": 10.0002232, "ext": { "intExt": 456, "stringExt": "擴充套件", "doubleExt": 456.001 } } / class TestCodable: NSObject, Codable { let int: Int let string: String let double: Double let ext: [String: Any]

enum Key: String, CodingKey {
    case int
    case string
    case double
    case ext
}

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: Key.self)
    try container.encode(int, forKey: .int)
    try container.encode(string, forKey: .string)
    try container.encode(double, forKey: .double)
    try container.encode(ext, forKey: .ext)
}

required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: Key.self)
    self.int = try container.decode(Int.self, forKey: .int)
    self.string = try container.decode(String.self, forKey: .string)
    self.double = try container.decode(Double.self, forKey: .double)
    self.ext = try container.decode(Dictionary<String, Any>.self, forKey: .ext)
}

} ```

擴充套件Codable

Dictionary<String, Any>的解析已經完成,其實Array<Any>的解析亦是同理。這裡把完整的擴充套件程式碼貼出,僅供參考: ```swift import Foundation

struct JSONCodingKeys: CodingKey { var stringValue: String

init(stringValue: String) {
    self.stringValue = stringValue
}

var intValue: Int?

init?(intValue: Int) {
    self.init(stringValue: "\(intValue)")
    self.intValue = intValue
}

}

extension KeyedDecodingContainer {

func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
    let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
    return try container.decode(type)
}

func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
    guard contains(key) else {
        return nil
    }
    return try decode(type, forKey: key)
}

func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
    var container = try self.nestedUnkeyedContainer(forKey: key)
    return try container.decode(type)
}

func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
    guard contains(key) else {
        return nil
    }
    return try decode(type, forKey: key)
}

func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
    var dictionary = Dictionary<String, Any>()

    for key in allKeys {
        if let boolValue = try? decode(Bool.self, forKey: key) {
            dictionary[key.stringValue] = boolValue
        } else if let stringValue = try? decode(String.self, forKey: key) {
            dictionary[key.stringValue] = stringValue
        } else if let intValue = try? decode(Int.self, forKey: key) {
            dictionary[key.stringValue] = intValue
        } else if let doubleValue = try? decode(Double.self, forKey: key) {
            dictionary[key.stringValue] = doubleValue
        } else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
            dictionary[key.stringValue] = nestedDictionary
        } else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
            dictionary[key.stringValue] = nestedArray
        }
    }
    return dictionary
}

}

extension UnkeyedDecodingContainer {

mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
    var array: [Any] = []
    while isAtEnd == false {
        if let value = try? decode(Bool.self) {
            array.append(value)
        } else if let value = try? decode(Double.self) {
            array.append(value)
        } else if let value = try? decode(String.self) {
            array.append(value)
        } else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
            array.append(nestedDictionary)
        } else if let nestedArray = try? decode(Array<Any>.self) {
            array.append(nestedArray)
        }
    }
    return array
}

mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {

    let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
    return try nestedContainer.decode(type)
}

}

extension KeyedEncodingContainerProtocol where Key == JSONCodingKeys { mutating func encode(_ value: Dictionary) throws { try value.forEach({ (key, value) in let key = JSONCodingKeys(stringValue: key) switch value { case let value as Bool: try encode(value, forKey: key) case let value as Int: try encode(value, forKey: key) case let value as String: try encode(value, forKey: key) case let value as Double: try encode(value, forKey: key) case let value as Dictionary: try encode(value, forKey: key) case let value as Array: try encode(value, forKey: key) case Optional.none: try encodeNil(forKey: key) default: throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath + [key], debugDescription: "Invalid JSON value")) } }) } }

extension KeyedEncodingContainerProtocol { mutating func encode(_ value: Dictionary?, forKey key: Key) throws { if value != nil { var container = self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key) try container.encode(value!) } }

mutating func encode(_ value: Array<Any>?, forKey key: Key) throws {
    if value != nil {
        var container = self.nestedUnkeyedContainer(forKey: key)
        try container.encode(value!)
    }
}

}

extension UnkeyedEncodingContainer { mutating func encode(_ value: Array) throws { try value.enumerated().forEach({ (index, value) in switch value { case let value as Bool: try encode(value) case let value as Int: try encode(value) case let value as String: try encode(value) case let value as Double: try encode(value) case let value as Dictionary: try encode(value) case let value as Array: try encode(value) case Optional.none: try encodeNil() default: let keys = JSONCodingKeys(intValue: index).map({ [$0] }) ?? [] throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath + keys, debugDescription: "Invalid JSON value")) } }) }

mutating func encode(_ value: Dictionary<String, Any>) throws {
    var nestedContainer = self.nestedContainer(keyedBy: JSONCodingKeys.self)
    try nestedContainer.encode(value)
}

} ```

GRDB的編解碼衝突

GRDB.swift

GRDB物件型資料庫的讀寫,有依賴Codable的編解碼。假如有一需求:

有一資料庫實體dbdb有一個Dictionary<String, Any>的物件aa在資料庫以TEXT型別存在。所以a在入庫前需要被打包成json串,以字串的形式寫入。同時,db實體需要支援Json編解碼。

相當合理的需求吧!但是問題來了,Codable可以藉助上述的擴充套件程式碼將a解析成Dictionary<String, Any>,但是由於GRDB重寫的Decoder協議以及KeyedEncodingContainerProtocolnestedContainer方法是沒有實現的,會導致崩潰 swift func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey { fatalError("not implemented") } 具體程式碼在GRDB.swift/FetchableRecord+Decodable.swift

GRDB是如何實現Codable的解析呼叫的呢?這裡以查詢為例GRDB的資料庫實體需要實現FetchableRecord協議,這個協議在原始碼中,若實體同時繼承了Decodable協議就會有一預設實現swift extension FetchableRecord where Self: Decodable { public init(row: Row) { // Intended force-try. FetchableRecord is designed for records that // reliably decode from rows. self = try! RowDecoder().decode(from: row) } }

該方法最後會呼叫到實體的decode方法進行例項化。所以解決問題的關鍵就是隻能將重寫該init(row: Row)方法避免與Json解析衝突,這裡引用官方文件的例子: ```swift struct Link: FetchableRecord { var url: URL var isVerified: Bool

init(row: Row) {
    url = row["url"]
    isVerified = row["verified"]
}

} ```

最後

本文主要歸納了筆者在開發當中使用Codable解析Dictionary<String, Any>的踩坑,利用這一技巧在使用Codable是也能靈活的解析一些異構的場景。當然如果考慮使用其他Json解析庫的話,alibaba/HandyJSON可能也是不錯的選擇。

如果有興趣的話也可以看看筆者的另一篇關於Codable的文章:關於Codable協議處理資料實體屬性預設值問題

參考: https://gist.github.com/loudmouth/332e8d89d8de2c1eaf81875cfcd22e24